diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a483e0a4e5..5ea07d2a2f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -318,9 +318,22 @@ jobs: run: | scripts/install_gitea.sh + - name: Setup Deno + uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + - name: Pre e2e + env: + FRONTEND_DEPLOY_IMAGE_TAG: ':${{ needs.get_version.outputs.frontend_sha }}' + PROCESSOR_DEPLOY_IMAGE_TAG: ':${{ needs.get_version.outputs.processor_sha }}' + NGINX_DEPLOY_IMAGE_TAG: ':${{ needs.get_version.outputs.nginx_sha }}' + GITEA_DEPLOY_IMAGE_TAG: ':${{ needs.get_version.outputs.gitea_sha }}' shell: bash - run: scripts/pre_e2e.sh + run: | + token=$(scripts/pre_e2e.sh) + docker-compose stop + echo "CYPRESS_GITEA_ADMIN_TOKEN=${token}" >> $GITHUB_ENV - name: e2e timeout-minutes: 40 diff --git a/README.md b/README.md index 0877913273..bdbb897b15 100644 --- a/README.md +++ b/README.md @@ -335,22 +335,39 @@ We configure our staging servers using [Ansible](https://docs.ansible.com/ansibl ```console docker-compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.e2e.yml up e2e ``` +## + +> Note: The GITEA_ADMIN_TOKEN is needed because the Gitea service is hidden and inaccessible to other containers. + +> Before running e2e tests locally make sure to source the `.env` file: ` set -o allexport && source .env && set +o allexport` + ### Without docker +Make sure that npm packages are installed + +```console +yarn --cwd e2e +``` +then you can run the tests with + ```console -cd e2e -yarn -yarn e2e +yarn --cwd e2e e2e --env GITEA_ADMIN_TOKEN=$(scripts/pre_e2e.sh) ``` ### GUI +Make sure that npm packages are installed ```console -cd e2e -yarn -yarn gui +yarn --cwd e2e ``` +then you can open the GUI with + +```console +yarn --cwd e2e gui --env GITEA_ADMIN_TOKEN=$(scripts/pre_e2e.sh) +``` + + ### Recording new visual tests: diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml index 741603be04..a7e063296d 100644 --- a/docker-compose.e2e.yml +++ b/docker-compose.e2e.yml @@ -22,6 +22,7 @@ services: - COMMIT_INFO_SHA=${COMMIT_INFO_SHA} - COMMIT_INFO_TIMESTAMP=${COMMIT_INFO_TIMESTAMP} - COMMIT_INFO_BRANCH=${COMMIT_INFO_BRANCH} + - CYPRESS_GITEA_ADMIN_TOKEN=${CYPRESS_GITEA_ADMIN_TOKEN} depends_on: - nginx working_dir: /e2e diff --git a/e2e/cypress/integration/IBOM.spec.js b/e2e/cypress/integration/IBOM.spec.js index 2fa4f0dfae..4bb214917d 100644 --- a/e2e/cypress/integration/IBOM.spec.js +++ b/e2e/cypress/integration/IBOM.spec.js @@ -10,28 +10,17 @@ describe('IBOM page', () => { }) it('should redirect to project page (multi project)', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() const multiProjectsNames = ['alpha-spectrometer', 'electron-detector'] const multiProjectsRepoName = 'DIY_particle_detector' const syncedRepoUrlMultiProjects = 'https://github.com/kitspace-test-repos/DIY_particle_detector' - /* Migrate the multiproject repo */ - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) + + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 120_000 }).as('projectCards') @@ -39,7 +28,7 @@ describe('IBOM page', () => { // Go to IBOM page for a subproject const subProjectName = multiProjectsNames[0] - cy.visit(`${username}/${multiProjectsRepoName}/${subProjectName}/IBOM`) + cy.visit(`${user.username}/${multiProjectsRepoName}/${subProjectName}/IBOM`) // Click on the title cy.get('#title').click() @@ -47,36 +36,26 @@ describe('IBOM page', () => { // It should redirect to the subproject page cy.url({ timeout: 20_000 }).should( 'eq', - `${ - Cypress.config().baseUrl - }/${username}/${multiProjectsRepoName}/${subProjectName}`, + `${Cypress.config().baseUrl}/${ + user.username + }/${multiProjectsRepoName}/${subProjectName}`, ) }) it('should redirect to the project page (normal project)', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - /* Migrate the normal repo */ - cy.forceVisit('/projects/new') + const user = getFakeUser() const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' const normalRepoName = 'CH330_Hardware' + cy.importRepo(syncedRepoUrl, normalRepoName, user) - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() - - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${normalRepoName}`) + cy.forceVisit(`/${user.username}/${normalRepoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') // Go to IBOM page for a single (not multi) project cy.get('[data-cy=ibom]').should('be.visible') - cy.visit(`${username}/${normalRepoName}/_/IBOM`) + cy.visit(`${user.username}/${normalRepoName}/_/IBOM`) // Click on the title cy.get('#title').click() @@ -84,24 +63,20 @@ describe('IBOM page', () => { // It should redirect to the project page cy.url({ timeout: 20_000 }).should( 'eq', - `${Cypress.config().baseUrl}/${username}/${normalRepoName}`, + `${Cypress.config().baseUrl}/${user.username}/${normalRepoName}`, ) }) it('should not render `Assembly Guide` button for projects with `ibom-enabled: false', () => { - const { username, email, password } = getFakeUser() - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') + const user = getFakeUser() /* Migrate a repo with `omit-ibom` set to `true` */ - cy.forceVisit('/projects/new') + const repoName = 'solarbird_shenzen_rdy' const IBOMDisabledRepoURL = 'https://github.com/kitspace-test-repos/solarbird_shenzen_rdy' - const repoName = 'solarbird_shenzen_rdy' - cy.get('[data-cy=sync-field]').type(IBOMDisabledRepoURL) - cy.get('button').contains('Sync').click() - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.importRepo(IBOMDisabledRepoURL, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) + cy.url({ timeout: 60_000 }).should('contain', `${user.username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 100_000 }).should('be.visible') // The ibom button shouldn't get rendered for this project. diff --git a/e2e/cypress/integration/IBOM.visual.spec.js b/e2e/cypress/integration/IBOM.visual.spec.js index 2cd332f887..3fd5319e94 100644 --- a/e2e/cypress/integration/IBOM.visual.spec.js +++ b/e2e/cypress/integration/IBOM.visual.spec.js @@ -2,27 +2,17 @@ import { getFakeUser } from '../support/getFakeUser' describe.skip('Regression test for IBOM ', () => { before(() => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'CH330_Hardware' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() - - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.importRepo(syncedRepoUrl, repoName, user) + cy.forceVisit(`/${user.username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') - cy.visit(`${username}/${repoName}/IBOM`) + cy.visit(`${user.username}/${repoName}/IBOM`) // Wait until the ibom is visible cy.get('.topmostdiv').should('be.visible') diff --git a/e2e/cypress/integration/authRedirects.spec.js b/e2e/cypress/integration/authRedirects.spec.js deleted file mode 100644 index 2efe14041a..0000000000 --- a/e2e/cypress/integration/authRedirects.spec.js +++ /dev/null @@ -1,187 +0,0 @@ -import { getFakeUser } from '../support/getFakeUser' - -describe('Authentication redirects', () => { - const { username, email, password } = getFakeUser() - - before(() => { - /* - * The purpose of this isn't actually visiting the homepage. - * Sometimes, the frontend has a slow startup time which results in a random failure. - */ - cy.visit('/') - cy.createUser(username, email, password) - }) - - it("should redirect unauthenticated users to '/login' when accessing require sign in page (server)", () => { - // Clear the cookies to make sure the user isn't authenticated - cy.clearCookies() - // `/project/new` is marked as require sign in. - cy.forceVisit('/projects/new') - cy.get('[data-cy=login-grid]').should('be.visible') - }) - - it("should redirect unauthenticated users to '/login' when accessing require sign in page (client)", () => { - // Clear the cookies to make sure the user isn't authenticated - cy.clearCookies() - cy.forceVisit('/') - // navigate to `/projects/new`with client-side interactions. - cy.get('[data-cy=add-project]').click({ timeout: 10000 }) - // `/project/new` is marked as require sign in. - cy.get('[data-cy=login-grid]').should('be.visible') - }) - - /* - ! There is no (client) version of this because there is no user interaction which will lead to redirecting to `/login` - ! when the user is already logged in. - */ - it('should redirect authenticated users to homepage when accessing reqSignOut page (server)', () => { - // Sign in - cy.visit('/login') - cy.signIn(username, password) - cy.get('[data-cy=user-menu]') - - // `/login` is marked as `reqSignOut`. - cy.visit('/login') - cy.get('[data-cy=cards-grid]').should('be.visible') - }) -}) - -describe('Redirect after login', () => { - const { username, email, password } = getFakeUser() - - before(() => { - cy.visit('/') - cy.createUser(username, email, password) - }) - - beforeEach(() => { - // deauthenticate the user and reload the page to update the CSRF token - cy.clearCookies() - cy.reload() - }) - - it('should redirect to homepage if there is no redirect query', () => { - cy.visit('/login') - // sign the user in. - cy.signIn(username, password) - - // After a successful login the user is redirect to the homepage. - cy.url().should('eq', `${Cypress.config().baseUrl}${Cypress.env('home_path')}`) - }) - - it('should redirect to correct page if there is a redirect query', () => { - const pageClickFrom = 'projects/new' - - cy.visit(pageClickFrom) - - // sign the user in. - cy.signIn(username, password) - // redirect to the page in the redirect query parameter - cy.url().should('eq', `http://kitspace.test:3000/${pageClickFrom}`) - }) - - it("should redirect to the `pathname` if it isn't `login`", () => { - const requireSignInPage = '/settings' - - cy.visit(requireSignInPage) - - cy.get('[data-cy=login-grid]').should('be.visible') - cy.signIn(username, password) - cy.url().should('eq', `http://kitspace.test:3000${requireSignInPage}`) - - cy.get('h1').contains('Settings') - }) -}) - -describe('Redirect after sign-up', () => { - it('should redirect to homepage if there is no redirect query', () => { - // sign the user up - const { username, email, password } = getFakeUser() - - cy.visit('/login') - cy.signUp(username, email, password) - - // After a successful sign up the user is redirect to the homepage. - cy.url().should('eq', `${Cypress.config().baseUrl}${Cypress.env('home_path')}`) - }) - - it('should redirect to correct page if there is a redirect query', () => { - const pageClickFrom = 'projects/new' - cy.visit(pageClickFrom) - // sign the user up - const { username, email, password } = getFakeUser() - - cy.signUp(username, email, password) - - // redirect to the page in the redirect query parameter - cy.url().should('eq', `http://kitspace.test:3000/${pageClickFrom}`) - }) - - it("should redirect to the `pathname` if it isn't `login`", () => { - const requireSignInPage = '/settings' - - cy.visit(requireSignInPage) - - cy.get('[data-cy=login-grid]').should('be.visible') - - // sign the user up - const { username, email, password } = getFakeUser() - cy.signUp(username, email, password) - - cy.url().should('eq', `http://kitspace.test:3000${requireSignInPage}`) - cy.get('h1').contains('Settings') - }) -}) - -describe('Redirect after logout', () => { - it('should redirect to login page', () => { - const { username, email, password } = getFakeUser() - cy.createUser(username, email, password) - - // Press the logout button - cy.get('img[alt=avatar]').trigger('mousemove').click() - cy.get('#logout').trigger('mousemove').click() - - // should redirect to `/login` - cy.url().should('eq', 'http://kitspace.test:3000/login') - }) -}) - -describe('"Add Project" behavior', () => { - const { username, email, password } = getFakeUser() - - before(() => { - cy.visit('/') - cy.createUser(username, email, password) - }) - - beforeEach(() => { - // deauthenticate the user and reload the page to update the CSRF token - cy.clearCookies() - cy.reload() - }) - - it('should redirect unauthenticated user to /login and then back to /projects/new', () => { - // The user is unauthenticated - cy.window().its('session.user').should('not.ok') - - // Clicking `Add Project` redirects to the login page. - // and adds redirect query to `/projects/new/` - cy.get('#add_project').click() - - const { username, email, password } = getFakeUser() - cy.signUp(username, email, password) - cy.url().should('eq', 'http://kitspace.test:3000/projects/new') - }) - - it('should redirect authenticated user to /projects/new', () => { - // sign the user in. - cy.visit('/login') - cy.signIn(username, password) - - // Clicking `Add Project` redirects to new project page. - cy.visit('/') - cy.get('#add_project').click() - cy.url().should('eq', 'http://kitspace.test:3000/projects/new') - }) -}) diff --git a/e2e/cypress/integration/legacyRedirect.spec.js b/e2e/cypress/integration/legacyRedirect.spec.js index 0ab86d0203..13d643d133 100644 --- a/e2e/cypress/integration/legacyRedirect.spec.js +++ b/e2e/cypress/integration/legacyRedirect.spec.js @@ -10,27 +10,16 @@ describe('Legacy redirects', () => { }) it('should redirect `/boards/*/user/project/*` to `/user/project/*`', () => { - const { username, email, password } = getFakeUser() - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() const multiProjectsRepoName = 'DIY_particle_detector' const multiProjectsNames = ['alpha-spectrometer', 'electron-detector'] const syncedRepoUrlMultiProjects = 'https://github.com/kitspace-test-repos/DIY_particle_detector' - /* Migrate the multiproject repo */ - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 120_000 }).as('projectCards') @@ -38,38 +27,27 @@ describe('Legacy redirects', () => { /* Legacy redirect */ cy.visit( - `/boards/github.com/${username}/${multiProjectsRepoName}/${multiProjectsNames[0]}`, + `/boards/github.com/${user.username}/${multiProjectsRepoName}/${multiProjectsNames[0]}`, ) cy.url().should( 'eq', - `${Cypress.config().baseUrl}/${username}/${multiProjectsRepoName}/${ + `${Cypress.config().baseUrl}/${user.username}/${multiProjectsRepoName}/${ multiProjectsNames[0] }`, ) }) it('should redirect `/interactive_bom/?*/user/project*` to `/user/project/IBOM`', () => { - const { username, email, password } = getFakeUser() - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - + const user = getFakeUser() const multiProjectsRepoName = 'DIY_particle_detector' const multiProjectsNames = ['alpha-spectrometer', 'electron-detector'] const syncedRepoUrlMultiProjects = 'https://github.com/kitspace-test-repos/DIY_particle_detector' /* Migrate the multiproject repo */ - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 120_000 }).as('projectCards') @@ -77,11 +55,11 @@ describe('Legacy redirects', () => { /* Legacy redirect */ cy.visit( - `/interactive_bom/?github.com/${username}/${multiProjectsRepoName}/${multiProjectsNames[0]}`, + `/interactive_bom/?github.com/${user.username}/${multiProjectsRepoName}/${multiProjectsNames[0]}`, ) cy.url().should( 'eq', - `${Cypress.config().baseUrl}/${username}/${multiProjectsRepoName}/${ + `${Cypress.config().baseUrl}/${user.username}/${multiProjectsRepoName}/${ multiProjectsNames[0] }/IBOM`, ) diff --git a/e2e/cypress/integration/multiProject.spec.js b/e2e/cypress/integration/multiProject.spec.js index 33ffd16184..c9e11cb359 100644 --- a/e2e/cypress/integration/multiProject.spec.js +++ b/e2e/cypress/integration/multiProject.spec.js @@ -17,22 +17,13 @@ describe('Render project cards', () => { }) it('should render a card for each multiproject', () => { - const { username, email, password } = getFakeUser() - + const user = getFakeUser() const repoName = syncedRepoUrlMultiProjects.split('/').slice(-1).toString() - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the multiproject repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, repoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.forceVisit(`/${user.username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 60_000 }).should( 'have.length', @@ -40,7 +31,7 @@ describe('Render project cards', () => { ) // should render a card for each multiproject - cy.visit(`/${username}`) + cy.visit(`/${user.username}`) multiProjectsNames.forEach(name => { cy.get('[data-cy=project-card]').contains(name) @@ -48,38 +39,26 @@ describe('Render project cards', () => { }) it('should display card thumbnail', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() // Migrate the multiproject repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 60_000 }).should( 'have.length', multiProjectsNames.length, ) - /* Migrate the normal repo */ - cy.forceVisit('/projects/new') + // Migrate the normal repo + cy.importRepo(syncedRepoUrl, normalRepoName, user) - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.forceVisit(`/${user.username}/${normalRepoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${normalRepoName}`) + // Wait for the repo to finish processing, by checking the visibility of info-bar. + cy.get('[data-cy=info-bar]', { timeout: 100_000 }).should('be.visible') - cy.visit(`/${username}`) + cy.visit(`/${user.username}`) // There should be 3 cards = 2 form multiprojects + 1 normal project cy.get('[data-cy=project-card]', { timeout: 60_000 }).should( 'have.length', @@ -88,23 +67,13 @@ describe('Render project cards', () => { }) it('should redirect to the multi project page', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() /* Migrate the multiproject repo */ - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) + + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 60_000 }).should( 'have.length', @@ -115,45 +84,34 @@ describe('Render project cards', () => { cy.visit('/') // Search for the project - cy.get('nav [data-cy=search-field] > input').type(username) + cy.get('nav [data-cy=search-field] > input').type(user.username) cy.get('nav [data-cy=search-form]').submit() // Click on a subproject project card cy.get('[data-cy=project-card]').within(() => { - cy.contains(username) + cy.contains(user.username) cy.contains(multiProjectName).click({ force: true }) }) - // Should redirect to the `[username]/[projectName]/[multiProject]` + // Should redirect to the `[user.username]/[projectName]/[multiProject]` cy.url({ timeout: 20_000 }).should( 'contain', - `${username}/${multiProjectsRepoName}/${multiProjectName}`, + `${user.username}/${multiProjectsRepoName}/${multiProjectName}`, ) }) it('should handle subproject names with URL invalid characters', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - + const user = getFakeUser() const projectWithInvalidCharsURL = 'https://github.com/kitspace-test-repos/open-visual-stimulator' const projectWithInvalidCharsName = 'open-visual-stimulator' const numberOfSubProjects = 4 /* Migrate the multiproject repo */ - cy.get('[data-cy=sync-field]').type(projectWithInvalidCharsURL) - cy.get('button').contains('Sync').click() + cy.importRepo(projectWithInvalidCharsURL, projectWithInvalidCharsName, user) + + cy.forceVisit(`/${user.username}/${projectWithInvalidCharsName}`) - // Wait for redirection for project page - cy.url({ timeout: 120_000 }).should( - 'contain', - `${username}/${projectWithInvalidCharsName}`, - ) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 60_000 }).should( 'have.length', @@ -175,22 +133,12 @@ describe('Multi project page', () => { }) it('should render the page components', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() // Migrate the multiproject repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) - cy.url({ timeout: 60_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 60_000 }).as('projectCards') @@ -201,12 +149,11 @@ describe('Multi project page', () => { cy.get('@projectCards').contains(subProjectName).click() cy.url({ timeout: 20_000 }).should( 'contain', - `${username}/${multiProjectsRepoName}/${subProjectName}`, + `${user.username}/${multiProjectsRepoName}/${subProjectName}`, ) // Different page elements should be visible. const pageComponents = [ - 'sync-msg', 'info-bar', 'board-showcase', 'board-showcase-top', @@ -224,22 +171,12 @@ describe('Multi project page', () => { }) it('should render the details from multi project in kitspace.yaml', () => { - const { username, email, password } = getFakeUser() - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const user = getFakeUser() // Migrate the multiproject repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrlMultiProjects) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrlMultiProjects, multiProjectsRepoName, user) - cy.url({ timeout: 10_000 }).should( - 'contain', - `${username}/${multiProjectsRepoName}`, - ) + cy.forceVisit(`/${user.username}/${multiProjectsRepoName}`) // Wait for the repo to finish processing, by checking the visibility sub projects cards. cy.get('[data-cy=project-card]', { timeout: 120_000 }).should( 'have.length', @@ -249,17 +186,17 @@ describe('Multi project page', () => { const multiProjectName = multiProjectsNames[0] cy.visit('/') // Search for the project - cy.get('nav [data-cy=search-field] > input').type(username) + cy.get('nav [data-cy=search-field] > input').type(user.username) cy.get('nav [data-cy=search-form]').submit() // Click on a multiproject project card cy.get('[data-cy=project-card]').within(() => { - cy.contains(username) + cy.contains(user.username) cy.contains(multiProjectName).click({ force: true }) }) cy.url({ timeout: 20_000 }).should( 'contain', - `${username}/${multiProjectsRepoName}/${multiProjectName}`, + `${user.username}/${multiProjectsRepoName}/${multiProjectName}`, ) // The info bar should have the correct title. cy.get('[data-cy=project-title]').should('have.text', multiProjectName) diff --git a/e2e/cypress/integration/readme.spec.js b/e2e/cypress/integration/readme.spec.js index 34de589aa5..dab9454ae9 100644 --- a/e2e/cypress/integration/readme.spec.js +++ b/e2e/cypress/integration/readme.spec.js @@ -10,24 +10,15 @@ describe('Relative README images URLs normalization', () => { }) it('handles readme images starting with `/`', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'CH330_Hardware' - const syncedRepoUrl = - 'https://github.com/kitspace-test-repos/CH330_Hardware' - - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') + const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.visit(`${user.username}/${repoName}`) cy.get('[data-cy=readme-img]', { timeout: 60_000 }) .should('have.attr', 'src') @@ -43,27 +34,19 @@ describe('Relative README images URLs normalization', () => { }) it('handles readmes in folders', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'readmes-in-folders' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/readmes-in-folders' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.visit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.visit(`${user.username}/${repoName}`) cy.get('[data-cy=project-card]', { timeout: 20_000 }).should('have.length', 2) - cy.visit(`/${username}/${repoName}/project_2`) + cy.visit(`/${user.username}/${repoName}/project_2`) cy.get('[data-cy=readme]').within(() => { cy.get('img').each($img => { // checks for naturalWidth/naturalHeight are not working @@ -79,27 +62,19 @@ describe('Relative README images URLs normalization', () => { }) it('redirects relative urls to original git service', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'readmes-in-folders' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/readmes-in-folders' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.visit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.visit(`${user.username}/${repoName}`) cy.get('[data-cy=project-card]', { timeout: 20_000 }).should('have.length', 2) - cy.visit(`/${username}/${repoName}/project_1`) + cy.visit(`/${user.username}/${repoName}/project_1`) cy.get('[data-cy=readme]').within(() => { cy.get('a').each($a => { @@ -111,24 +86,17 @@ describe('Relative README images URLs normalization', () => { describe('Readme style', () => { it('should auto link readme and summary links', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'readme-and-summary-auto-link' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/readme-and-summary-auto-link' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.visit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') @@ -150,23 +118,16 @@ describe('Readme style', () => { }) it('renders :emoji: in readme and project description', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'ogx360' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/ogx360' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.visit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') @@ -176,23 +137,16 @@ describe('Readme style', () => { }) it('preserves URLs for GitHub Actions badges', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'ogx360' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/ogx360' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.visit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') diff --git a/e2e/cypress/integration/search.spec.js b/e2e/cypress/integration/search.spec.js index f180cc78f1..afc1693411 100644 --- a/e2e/cypress/integration/search.spec.js +++ b/e2e/cypress/integration/search.spec.js @@ -17,23 +17,16 @@ describe('Navbar search', () => { }) it('should display project card on submitting search form', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'CH330_Hardware' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish migration, by checking the visibility of processing-loader. cy.get('[data-cy=processing-loader]', { timeout: 60_000 }) // Wait for the repo to finish processing, by checking the visibility of info-bar. @@ -52,23 +45,16 @@ describe('Navbar search', () => { describe('Homepage search on mobile', () => { it('should display project card on submitting search form', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'CH330_Hardware' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field] > input').type(syncedRepoUrl, { force: true }) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish migration, by checking the visibility of processing-loader. cy.get('[data-cy=processing-loader]', { timeout: 60_000 }) // Wait for the repo to finish processing, by checking the visibility of info-bar. @@ -97,23 +83,16 @@ describe('/search route', () => { }) it('should use `q` from query parameters', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'HACK' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/HACK' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish migration, by checking the visibility of processing-loader. cy.get('[data-cy=processing-loader]', { timeout: 60_000 }) // Wait for the repo to finish processing, by checking the visibility of info-bar. diff --git a/e2e/cypress/integration/signInForm.spec.js b/e2e/cypress/integration/signInForm.spec.js deleted file mode 100644 index 3e98d09dc7..0000000000 --- a/e2e/cypress/integration/signInForm.spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import faker from 'faker' - -import { getFakeUser } from '../support/getFakeUser' - -describe('Log in form validation', () => { - before(() => { - cy.visit('/login') - cy.get('[data-cy=login-grid] a').contains('Login').click({ force: true }) - }) - it('should validate username field', () => { - const invalidUsernames = ['abc ', 'abc@', ' ', '^', 'longusername'.repeat(4)] - - // Type the password to make sure we're validating the username only - cy.get('input[name=password]').type('123456') - - invalidUsernames.forEach(username => { - cy.get('input[name=username]').clear().type(username).blur() - cy.get('.prompt.label').as('message') - - // Validation error message should appear. - cy.get('@message').should('be.visible') - - // The error message should indicate that the username is invalid. - cy.get('@message') - .contains( - /Invalid "username"\. Username must contain only letters, numbers, "_", "-", and "\."|"username" length must be less than or equal to 40 characters long/g, - ) - .should('be.visible') - - // Login button should stay disabled - cy.get('button').contains('Login').should('be.disabled') - }) - }) -}) - -describe('Log in form submission', () => { - const { username, email, password } = getFakeUser() - - before(() => { - // create user and log him in. - cy.createUser(username, email, password) - cy.clearCookies() - cy.visit('/login') - cy.get('[data-cy=login-grid] a').contains('Login').click({ force: true }) - }) - - it('should display error message on submitting form with wrong username', () => { - cy.signIn('nonRegUser', password) - cy.get('.negative').should('include.text', 'Wrong username or password') - }) - - it('should display error message on submitting form with wrong password', () => { - cy.signIn(username, 'wrong_password_1234') - cy.get('.negative').should('include.text', 'Wrong username or password') - }) -}) diff --git a/e2e/cypress/integration/signUpForm.spec.js b/e2e/cypress/integration/signUpForm.spec.js deleted file mode 100644 index e91b078410..0000000000 --- a/e2e/cypress/integration/signUpForm.spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import { getFakeUser } from '../support/getFakeUser' - -describe('Sign up form validation', () => { - before(() => { - cy.visit('/login') - cy.get('a').contains('Sign up').click({ force: true }) - }) - - afterEach(() => { - // All the tests are invalid forms which should keep the button inactive. - cy.get('button').contains('Sign up').should('be.disabled') - }) - - it('should route to sign up form based on params', () => { - // The form is rendered on screen. - cy.contains('Create a new account') - }) - - it('should display error message on using invalid username', () => { - // Try different invalid usernames. - const invalidUsernames = ['abc ', 'abc@', ' ', '^', 'longusername'.repeat(4)] - - invalidUsernames.forEach(username => { - cy.get('input[name=username]').clear().type(username).blur() - - // The error message should indicate that the username is invalid. - cy.get('.prompt.label').as('message') - cy.get('@message') - .contains( - /Invalid "username"\. Username must contain only letters, numbers, "_", "-", and "\."|"username" length must be less than or equal to 40 characters long|"username" length must be at least 2 characters long/g, - ) - .should('be.visible') - cy.get('@message').should('include.text', '"username"') - }) - }) - - it('should display error message on using invalid email', () => { - // Try different invalid emails. - const invalidEmails = ['abc ', 'abc@', ' ', '^', 'www.google.com'] - - cy.get('input[name=username]').clear().type('someone') - - invalidEmails.forEach(email => { - cy.get('input[name=email]').clear().type(email).blur() - - // The error message should indicate that the email is invalid. - cy.get('.prompt.label').as('message') - cy.get('@message').should('be.visible') - cy.get('@message').should('include.text', 'Invalid email address') - }) - }) - - it('should display error message on using invalid password', () => { - // Try different invalid password. - cy.get('input[name=username]').clear().type('someone') - cy.get('input[name=email]').clear().type('someone@example.com') - cy.get('input[name=password]').clear().type('12345').blur() - - // The error message should indicate that the password is invalid. - cy.get('.prompt.label').as('message') - cy.get('@message').should('be.visible') - cy.get('@message').should('include.text', '"password"') - }) -}) - -describe('Sign up form submission', () => { - const { username, email, password } = getFakeUser() - - before(() => { - /* - * The purpose of this isn't actually visiting the homepage. - * Sometimes, the frontend has a slow startup time which results in a random failure. - */ - cy.visit('/') - cy.clearCookies() - - // Create user used for conflicts test then sign out again. - cy.visit('/login') - cy.signUp(username, email, password) - cy.clearCookies() - }) - - beforeEach(() => { - cy.visit('/login') - }) - - it('should automatically sign the user in after submitting a valid form', () => { - const { username, email, password } = getFakeUser() - - cy.signUp(username, email, password) - - // the user should be signed in - cy.get('[data-cy=user-menu]').should('be.visible') - }) - - it('should display error message on submitting a from with used username', () => { - cy.signUp(username, email, password) - - // The error message should indicate that username is already taken. - cy.get('.negative').as('message') - cy.get('@message').should('be.visible') - cy.get('@message').should('include.text', 'User already exists.') - }) - - it('should display error message on submitting a from with used email', () => { - cy.signUp('newUser', email, password) - - // The error message should indicate that this email is already registered. - cy.get('.negative').as('message') - cy.get('@message').should('be.visible') - cy.get('@message').should('include.text', 'Email is already used.') - }) - - it('should display error message on submitting a from with reserved username', () => { - const reservedNames = ['admin', 'user'] // Not a full list of Gitea reserved names. - - reservedNames.forEach(name => { - cy.signUp(name, email, password) - - // The error message should indicate that the username is reserved. - cy.get('.negative').as('message') - cy.get('@message').should('be.visible') - cy.get('@message').should('include.text', 'Name is reserved.') - }) - }) -}) - -describe('Already have an account? Log in here.', () => { - it('should open login pane on clicking "Already have an account? Log in here."', () => { - cy.visit('/login') - cy.get('[data-cy=log-in-here]').click({ force: true }) - cy.get('h2').contains('Login') - }) -}) diff --git a/e2e/cypress/integration/syncRepo.spec.js b/e2e/cypress/integration/syncRepo.spec.js index 0d2a184574..32f5202b14 100644 --- a/e2e/cypress/integration/syncRepo.spec.js +++ b/e2e/cypress/integration/syncRepo.spec.js @@ -9,13 +9,13 @@ describe('Syncing a project behavior validation', () => { cy.visit('/') }) - it('should sync a repo on gitea', () => { + it.skip('should sync a repo via UI', () => { const syncedRepoUrl = 'https://github.com/AbdulrhmnGhanem/light-test-repo' const repoName = 'light-test-repo' - const { username, email, password } = getFakeUser() + const user = getFakeUser() - cy.createUser(username, email, password) + cy.createUser(user.username, email, password) cy.visit('/') cy.get('[data-cy=user-menu]') @@ -25,32 +25,24 @@ describe('Syncing a project behavior validation', () => { cy.get('[data-cy=sync-result-message]').should('have.class', 'green') // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.url({ timeout: 60_000 }).should('contain', `${user.username}/${repoName}`) cy.get('[data-cy=project-title]') - cy.visit(`/${username}`) + cy.visit(`/${user.username}`) // assert the repo is on the user's project page cy.get('[data-cy=project-card]').contains(repoName) }) it('should display original url for synced repos', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'CH330_Hardware' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) + cy.forceVisit(`/${user.username}/${repoName}`) // The info bar should include the original url cy.get('[data-cy=original-url] > a', { timeout: 60_000 }) @@ -61,24 +53,17 @@ describe('Syncing a project behavior validation', () => { ) }) - it.skip('should fall back to repo description on missing `summary` key in `kitspace.yaml`', () => { - const { username, email, password } = getFakeUser() + it('should fall back to repo description on missing `summary` key in `kitspace.yaml`', () => { + const user = getFakeUser() const repoName = 'ArduTouch' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/ArduTouch' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') @@ -86,32 +71,25 @@ describe('Syncing a project behavior validation', () => { cy.get('[data-cy=project-description]').should('contain', 'ARDUTOUCH ') }) - it.skip('should fall back to repo description in `ProjectCard` on missing `summary` in `kitspace.yaml', () => { - const { username, email, password } = getFakeUser() + it('should fall back to repo description in `ProjectCard` on missing `summary` in `kitspace.yaml', () => { + const user = getFakeUser() const repoName = 'CH330_Hardware_without_summary' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/CH330_Hardware_without_summary' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=info-bar]', { timeout: 60_000 }).should('be.visible') // Go to user projects page to limit the results to the project synced above - cy.visit(`/${username}`) + cy.visit(`/${user.username}`) - cy.get('[data-cy=project-card]').should('contain', username).and( + cy.get('[data-cy=project-card]').should('contain', user.username).and( 'contain.text', // This is github repo description. 'To test that meilisearch falls back to repo description', @@ -119,27 +97,20 @@ describe('Syncing a project behavior validation', () => { }) it.skip('should escape and render project description correctly', () => { - const { username, email, password } = getFakeUser() + const user = getFakeUser() const repoName = 'LED-Zappelin' const syncedRepoUrl = 'https://github.com/kitspace-test-repos/LED-Zappelin' - cy.createUser(username, email, password) - cy.visit('/') - cy.get('[data-cy=user-menu]') - - cy.forceVisit('/projects/new') - // Migrate the repo - cy.get('[data-cy=sync-field]').type(syncedRepoUrl) - cy.get('button').contains('Sync').click() + cy.importRepo(syncedRepoUrl, repoName, user) + + cy.forceVisit(`/${user.username}/${repoName}`) - // Wait for redirection for project page - cy.url({ timeout: 60_000 }).should('contain', `${username}/${repoName}`) // Wait for the repo to finish processing, by checking the visibility of info-bar. cy.get('[data-cy=project-card]', { timeout: 60_000 }) // Go to the PCB stimulator sub-project - cy.visit(`${username}/${repoName}/PCB-Stimulator`) + cy.visit(`${user.username}/${repoName}/PCB-Stimulator`) cy.get('[data-cy=project-description]').should( 'contain', diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index 9876e079d9..c363b6d02f 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -1,3 +1,5 @@ +import 'cypress-wait-until' + const signUpEndpoint = 'http://gitea.kitspace.test:3000/user/kitspace/sign_up' const signInEndpoint = 'http://gitea.kitspace.test:3000/user/kitspace/sign_in' @@ -59,3 +61,104 @@ Cypress.Commands.add('forceVisit', (path, timeout) => { cy.visit('/', { timeout }) cy.window().then(win => win.open(path, '_self')) }) + +Cypress.Commands.add('importRepo', (remoteUrl, repoName, user) => { + cy.createGiteaUser(user).then(giteaUser => { + cy.mirrorRepo(remoteUrl, repoName, giteaUser) + }) + cy.waitForStatusCode( + `http://gitea.kitspace.test:3000/${user.username}/${repoName}`, + ) +}) + +Cypress.Commands.add('mirrorRepo', (remoteRepo, repoName, user) => { + const endpoint = 'http://gitea.kitspace.test:3000/api/v1/repos/migrate' + const headers = { + 'Content-Type': 'application/json', + Authorization: `token ${Cypress.env('GITEA_ADMIN_TOKEN')}`, + } + + const giteaOptions = { + clone_addr: remoteRepo, + uid: user.id, + repo_name: repoName, + mirror: true, + wiki: false, + private: false, + pull_requests: false, + releases: true, + issues: false, + service: 'github', + } + + cy.request({ + url: endpoint, + method: 'POST', + headers, + body: JSON.stringify(giteaOptions), + failOnStatusCode: false, + }).then(response => { + if (!response.status === 201) { + throw new Error('Failed to mirror repo') + } + }) +}) + +Cypress.Commands.add('createGiteaUser', user => { + const headers = { + 'Content-Type': 'application/json', + Authorization: `token ${Cypress.env('GITEA_ADMIN_TOKEN')}`, + } + + const url = 'http://gitea.kitspace.test:3000/api/v1/admin/users' + + // check if the user already exists + cy.request({ + url: `http://gitea.kitspace.test:3000/api/v1/users/${user.username}`, + method: 'GET', + headers, + failOnStatusCode: false, + }).then(response => { + if (response.status === 200) { + return response.body + } + // if the user doesn't exist, create a new user and return the user object. + return cy + .request({ + url, + method: 'POST', + headers, + body: JSON.stringify(user), + failOnStatusCode: false, + }) + .then(response => { + if (response.status !== 201) { + throw new Error('Failed to create user') + } + return response.body + }) + }) +}) + +Cypress.Commands.add( + 'waitForStatusCode', + (url, timeout = 60_000, interval = 1000) => { + cy.log(`Waiting for HTTP request to ${url} to return status 200...`) + + cy.waitUntil( + () => { + return cy + .request({ url, method: 'GET', failOnStatusCode: false }) + .its('status') + .then(status => { + if (status === 200) { + return true + } + cy.log(`Got status ${status}, retrying...`) + return false + }) + }, + { timeout, interval }, + ) + }, +) diff --git a/e2e/cypress/support/index.d.ts b/e2e/cypress/support/index.d.ts index 8bc6b1b0d9..2529fe3ca5 100644 --- a/e2e/cypress/support/index.d.ts +++ b/e2e/cypress/support/index.d.ts @@ -27,7 +27,7 @@ declare namespace Cypress { * @example * cy.signIn(username, password) */ - signIn(username: string, password: string): Chainable + signIn(username: string, password: string): Chainable } interface Chainable { @@ -59,7 +59,7 @@ declare namespace Cypress { * @param newProject if the project is new wait for `@create` and `@upload`. * cy.get('.dropzone').dropFile(file, 'example.txt') */ - dropFiles(files, fileNames: string[], username: string, newProject=true): Chainable + dropFiles(files, fileNames: string[], username: string, newProject: boolean): Chainable } interface Chainable { @@ -70,4 +70,14 @@ declare namespace Cypress { */ forceVisit(path: string, timeout: number): Chainable } + + interface Chainable { + /** + * Import a repo from a url + * @param remoteUrl + * @param repoName + * @param user + */ + importRepo(remoteUrl: string, repoName: string, user: any): Chainable + } } diff --git a/e2e/package.json b/e2e/package.json index 3f54428a5d..26571258d0 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,8 +1,8 @@ { "name": "kitspace-e2e", "scripts": { - "e2e": "npx cypress run", - "gui": "npx cypress open", + "e2e": "yarn cypress run", + "gui": "yarn cypress open", "fmt": "prettier '*.json' cypress/ --write", "lint": "eslint cypress/" }, @@ -13,6 +13,7 @@ "cypress-fail-fast": "^3.3.0", "cypress-image-snapshot": "^4.0.1", "cypress-terminal-report": "^3.4.1", + "cypress-wait-until": "^1.7.2", "eslint": ">=7.0.0", "eslint-config-prettier": "8.3.0", "eslint-import-resolver-alias": "^1.1.2", diff --git a/e2e/yarn.lock b/e2e/yarn.lock index 31925c500e..ff697d0b51 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -505,6 +505,11 @@ cypress-terminal-report@^3.4.1: semver "^7.3.5" tv4 "^1.3.0" +cypress-wait-until@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/cypress-wait-until/-/cypress-wait-until-1.7.2.tgz#7f534dd5a11c89b65359e7a0210f20d3dfc22107" + integrity sha512-uZ+M8/MqRcpf+FII/UZrU7g1qYZ4aVlHcgyVopnladyoBrpoaMJ4PKZDrdOJ05H5RHbr7s9Tid635X3E+ZLU/Q== + cypress@^9.4.1: version "9.5.3" resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.3.tgz#7c56b50fc1f1aa69ef10b271d895aeb4a1d7999e" diff --git a/frontend/package.json b/frontend/package.json index b953c72a01..f74b7df505 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,7 +14,6 @@ "@justinribeiro/lite-youtube": "^1.3.1", "express": "^4.17.3", "file-loader": "^6.0.0", - "file-selector": "^0.2.4", "highlight.js": "^11.6.0", "joi": "^17.2.1", "lodash": "^4.17.21", @@ -27,10 +26,7 @@ "react": "^17.0.2", "react-dom": "^17.0.2", "react-double-scrollbar": "^0.0.15", - "react-dropzone": "^11.5.1", - "react-hot-toast": "^1.0.1", "react-intersection-observer": "^9.4.1", - "react-social-login-buttons": "3.1.2", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^2.0.3", "swr": "^1.3.0", diff --git a/frontend/src/components/NavBar/UserMenu.jsx b/frontend/src/components/NavBar/UserMenu.jsx deleted file mode 100644 index 30f18654a9..0000000000 --- a/frontend/src/components/NavBar/UserMenu.jsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useContext, useState } from 'react' -import { Menu, Icon, Popup } from 'semantic-ui-react' -import Link from 'next/link' -import Image from 'next/image' -import { useRouter } from 'next/router' - -import styles from './UserMenu.module.scss' -import { AuthContext } from '@contexts/AuthContext' - -export const UserDropDownMenu = () => { - const { user } = useContext(AuthContext) - const [isOpen, setIsOpen] = useState(false) - - const href = `/${user.username}` - return ( - - - avatar - - - } - onClose={() => setIsOpen(false)} - onOpen={() => setIsOpen(true)} - > - - - - - - - - - ) -} - -export const UserMenuItems = () => { - return ( - <> - - - - Settings - - - - - ) -} - -const LogoutButton = () => { - const { push } = useRouter() - const { logout } = useContext(AuthContext) - - const onClick = async () => { - const loggedOut = await logout() - - if (loggedOut) { - push('/login') - } - } - - return ( - - - Log out - - ) -} diff --git a/frontend/src/components/NavBar/UserMenu.module.scss b/frontend/src/components/NavBar/UserMenu.module.scss deleted file mode 100644 index 285e16c005..0000000000 --- a/frontend/src/components/NavBar/UserMenu.module.scss +++ /dev/null @@ -1,40 +0,0 @@ -.userName { - margin-bottom: 10px; - text-align: center; -} - -.userName a:hover { - text-decoration: none; - border-radius: 20%; - background-color: #efeaea; - padding: 5px; -} -.menu { - border: none !important; -} - -.userMenuIcon { - padding-top: 0 !important; - padding-bottom: 0 !important; -} - -.userDropDownMenuContainer { - display: flex; - align-items: center; -} - -.userDropDownMenuContainer:hover { - text-decoration: none; -} -.userDropDownMenuPopup { - // Make the drop-down menu drops from the arrow icon - transform: translate(8px, -5px) !important; -} - -.userDropDownMenuPopup *:hover { - text-decoration: none; -} - -.userImage { - border-radius: 20%; -} diff --git a/frontend/src/components/NavBar/index.jsx b/frontend/src/components/NavBar/index.jsx index d6f9e59ec1..573ed0999c 100644 --- a/frontend/src/components/NavBar/index.jsx +++ b/frontend/src/components/NavBar/index.jsx @@ -1,12 +1,9 @@ -import React, { useContext } from 'react' -import { bool } from 'prop-types' +import React from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import { Button, Icon, Menu, Popup } from 'semantic-ui-react' -import { AuthContext } from '@contexts/AuthContext' import SearchBar from './SearchBar' -import { UserMenuItems, UserDropDownMenu } from './UserMenu' import styles from './index.module.scss' import logoSvg from './logo.svg' @@ -44,7 +41,6 @@ const BigBar = () => ( - @@ -66,7 +62,6 @@ const SmallBar = () => ( } > - @@ -78,7 +73,10 @@ const SmallBar = () => ( const AddProjectButton = () => { return ( - + - - - ) -} - -UserControllerButton.propTypes = { - smallNavBar: bool, -} - export default NavBar diff --git a/frontend/src/components/NewProject/ConflictModal.tsx b/frontend/src/components/NewProject/ConflictModal.tsx deleted file mode 100644 index 67b40a38bf..0000000000 --- a/frontend/src/components/NewProject/ConflictModal.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useContext, useEffect } from 'react' -import { Button, Form, Input, Modal } from 'semantic-ui-react' - -import useForm from '@hooks/useForm' -import ExistingProjectFromModel from '@models/ExistingProjectForm' -import { isUsableProjectName } from '@utils/giteaApi' -import { formatAsGiteaRepoName } from '@utils/index' -import { AuthContext } from '@contexts/AuthContext' - -const ConflictModal = ({ - conflictModalOpen, - onClose, - originalProjectName, - message, - onDifferentName, - onDifferentNameButtonContent, - onOverwrite, - onOverwriteButtonContent, -}: BaseConflictModalProps) => { - const { user } = useContext(AuthContext) - const { form, onChange, isValid, populate, formatErrorPrompt } = useForm( - ExistingProjectFromModel, - ) - - const [isValidProjectName, setIsValidProjectName] = React.useState(false) - - const didChangeName = originalProjectName !== form.name - const projectName = formatAsGiteaRepoName(form.name || '') - - useEffect(() => { - populate({ name: originalProjectName }) - }, [originalProjectName, populate]) - - useEffect(() => { - if (form.name && didChangeName) { - isUsableProjectName(user.username, form.name, isValid).then( - setIsValidProjectName, - ) - } - }, [user, form.name, isValid, didChangeName]) - - return ( - - A project with that name already exists. - -

{message}

-
- - -
- - {didChangeName ? ( -