├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── cypress.config.js ├── cypress.env.example.json ├── cypress ├── e2e │ ├── api │ │ ├── group │ │ │ └── createGroup.cy.js │ │ ├── index.cy.js │ │ ├── project │ │ │ └── createProject.cy.js │ │ └── user │ │ │ ├── createUser.cy.js │ │ │ └── updateUser.cy.js │ └── gui │ │ ├── admin │ │ ├── broadcastMessage.cy.js │ │ └── impersonateUser.cy.js │ │ ├── allButProject.cy.js │ │ ├── authentication │ │ ├── login.cy.js │ │ ├── loginAsNonDefaultUser.cy.js │ │ └── logout.cy.js │ │ ├── group │ │ ├── createGroup.cy.js │ │ ├── createGroupLabel.cy.js │ │ ├── removeGroup.cy.js │ │ └── subGroup.cy.js │ │ ├── profile │ │ ├── createAccessToken.cy.js │ │ ├── deleteAccessTokens.cy.js │ │ └── setStatus.cy.js │ │ ├── project │ │ ├── assignIssue.cy.js │ │ ├── closeIssue.cy.js │ │ ├── closeIssueQuickAction.cy.js │ │ ├── commentOnIssue.cy.js │ │ ├── createIssue.cy.js │ │ ├── createNewFile.cy.js │ │ ├── createProject.cy.js │ │ ├── createProjectMilestone.cy.js │ │ ├── createWiki.cy.js │ │ ├── issueBoard.cy.js │ │ ├── issueMilestone.cy.js │ │ ├── labelAnIssue.cy.js │ │ ├── multipleUsersInAnProject.cy.js │ │ ├── projectButIssue.cy.js │ │ ├── projectIssue.cy.js │ │ ├── reopenClosedIssue.cy.js │ │ └── starProject.cy.js │ │ └── snippets │ │ └── createSnippet.cy.js ├── fixtures │ └── sampleUser.json └── support │ ├── commands │ ├── api_commands.js │ ├── gui_commands.js │ └── session_login.js │ ├── e2e.js │ ├── esbuild-preprocessor.js │ └── tasks │ └── index.js ├── package-lock.json └── package.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Cypress Tests Workflow' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | static-analysis: 15 | runs-on: ubuntu-22.04 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Run Standard JS 24 | run: npm run lint 25 | 26 | api-cypress-tests: 27 | needs: static-analysis 28 | runs-on: ubuntu-22.04 29 | services: 30 | gitlab-ce: 31 | image: wlsf82/gitlab-ce:latest 32 | ports: 33 | - 80:80 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Cypress run 39 | uses: cypress-io/github-action@v6 40 | env: 41 | CYPRESS_user_name: ${{ secrets.CYPRESS_user_name }} 42 | CYPRESS_user_password: ${{ secrets.CYPRESS_user_password }} 43 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 44 | with: 45 | command: npm run test:api:cloud 46 | 47 | gui-project-issue-cypress-tests: 48 | needs: static-analysis 49 | runs-on: ubuntu-22.04 50 | services: 51 | gitlab-ce: 52 | image: wlsf82/gitlab-ce:latest 53 | ports: 54 | - 80:80 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v4 58 | 59 | - name: Cypress run 60 | uses: cypress-io/github-action@v6 61 | env: 62 | CYPRESS_user_name: ${{ secrets.CYPRESS_user_name }} 63 | CYPRESS_user_password: ${{ secrets.CYPRESS_user_password }} 64 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 65 | with: 66 | command: npm run test:gui:project:issue:cloud 67 | 68 | gui-project-but-issue-cypress-tests: 69 | needs: static-analysis 70 | runs-on: ubuntu-22.04 71 | services: 72 | gitlab-ce: 73 | image: wlsf82/gitlab-ce:latest 74 | ports: 75 | - 80:80 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | 80 | - name: Cypress run 81 | uses: cypress-io/github-action@v6 82 | env: 83 | CYPRESS_user_name: ${{ secrets.CYPRESS_user_name }} 84 | CYPRESS_user_password: ${{ secrets.CYPRESS_user_password }} 85 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 86 | with: 87 | command: npm run test:gui:project:but:issue:cloud 88 | 89 | gui-all-but-project-cypress-tests: 90 | needs: static-analysis 91 | runs-on: ubuntu-22.04 92 | services: 93 | gitlab-ce: 94 | image: wlsf82/gitlab-ce:latest 95 | ports: 96 | - 80:80 97 | steps: 98 | - name: Checkout 99 | uses: actions/checkout@v4 100 | 101 | - name: Cypress run 102 | uses: cypress-io/github-action@v6 103 | env: 104 | CYPRESS_user_name: ${{ secrets.CYPRESS_user_name }} 105 | CYPRESS_user_password: ${{ secrets.CYPRESS_user_password }} 106 | CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} 107 | with: 108 | command: npm run test:gui:all:but:project:cloud 109 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | cypress.env.json 3 | cypress/downloads/ 4 | cypress/screenshots/ 5 | cypress/videos/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/standard/standard 3 | rev: v17.0.0 4 | hooks: 5 | - id: standard 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Walmyr Lima e Silva Filho 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitLab Cypress 2 | 3 | [![main](https://github.com/wlsf82/gitlab-cypress/actions/workflows/ci.yml/badge.svg)](https://github.com/wlsf82/gitlab-cypress/actions) 4 | [![gitlab-cypess](https://img.shields.io/endpoint?url=https://cloud.cypress.io/badge/simple/vxwq6z/main&style=flat&logo=cypress)](https://cloud.cypress.io/projects/vxwq6z/runs) 5 | 6 | Sample project to experiment with [Cypress](https://cypress.io) to test the GitLab application. 7 | 8 | ## Pre-requirements 9 | 10 | You need to have a GitLab local environment such as [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) or Docker up and running. 11 | 12 | You also need to have [Node.js](https://nodejs.org/) and npm installed on your computer. 13 | 14 | For this project, the following versions of Node.js and npm were used: 15 | 16 | ```sh 17 | $ node -v 18 | v20.16.0 19 | 20 | $ npm -v 21 | 10.9.0 22 | ``` 23 | 24 | ### Running GitLab on Docker 25 | 26 | Run `docker run --publish 80:80 --name gitlab --hostname localhost wlsf82/gitlab-ce` and wait for the environment to be up and running (this might take a minute or so). 27 | 28 | All should be ok if, when accessing the http://localhost/ URL, a form to define the password of the `root` user is displayed. 29 | 30 | > ❗**THERE'S NO NEED TO DEFINE THE PASSWORD MANUALLY**❗ 31 | > 32 | > There's an automated test for it. 😉 33 | > 34 | > Keep reading. 35 | 36 | ## Installation 37 | 38 | Run `npm i` to install the dev dependencies. 39 | 40 | ## Tests 41 | 42 | > Before running the tests, create a file called `cypress.env.json` in the project root directory, based on the [`cypress.env.example.json`](./cypress.env.example.json) file, and update the value of the `user_password` property with one of your choice. 43 | > 44 | > By default, the tests will run against `http://localhost/`, but if you need to run them in a different URL (e.g.: `http://localhost:3000/`), change the `baseUrl` property in the [`cypress.config.js`](./cypress.config.js) file. 45 | 46 | ### Headless mode 47 | 48 | Run `npm test` to run all tests in headless mode. 49 | 50 | Run `npm run test:api` to run only the API tests in headless mode. 51 | 52 | Run `npm run test:gui:project:issue` to run only the GUI Project-issue-related tests in headless mode. 53 | 54 | Run `npm run test:gui:project:but:issue` to run only the GUI Project-but-issue-related tests in headless mode. 55 | 56 | Run `npm run test:gui:all:but:project` to run only the GUI Project-not-related tests in headless mode. 57 | 58 | ### Interactive mode 59 | 60 | 1. Run `npm run cy:open` to open the Cypress App; 61 | 2. Select E2E Testing; 62 | 3. Select one of the available browsers (e.g., Electron), and click the Start button; 63 | 4. **Run the [`cypress/e2e/gui/profile/createAccessToken.cy.js`](./cypress/e2e/gui/profile/createAccessToken.cy.js) test;** 64 | 5. Finally, click on the test file you want to run and wait for it to finish. 65 | 66 | > **Important notes about the above steps** 67 | > 68 | > **Do not skip step 4!** It will authenticate the `root` user, create a GitLab Access Token, and make it available to all other tests while the Cypress App is kept open unless the [`cypress/e2e/gui/profile/deleteAccessTokens.cy.js`](./cypress/e2e/gui/profile/deleteAccessTokens.cy.js) test is run. In such a case, the [`cypress/e2e/gui/profile/createAccessToken.cy.js`](./cypress/e2e/gui/profile/createAccessToken.cy.js) test needs to be re-run. 69 | > 70 | > Also, step 4 creates a session for the `root` user, which will be restored by most tests. This means that login via GUI should only happens once, speeding up the execution. 🏎️ 71 | 72 | #### Example 73 | 74 | Here's an example of running all the GUI tests in interactive mode. 75 | 76 | https://user-images.githubusercontent.com/2768415/225186210-4dd51c26-9baf-4e65-9b09-6f79a818a7d5.mp4 77 | 78 | ## Contributing 79 | 80 | If you want to contribute to this project, follow the below steps. 81 | 82 | 1. Fork the project; 83 | 2. Clone your fork and make your changes; 84 | 3. Test your changes locally, and move on only when all tests are green; 85 | 4. Push your changes to GitHub and create a pull request (PR); 86 | 5. After the GitHub Workflow of your PR is green, tag @wlsf82, ask for review and wait for feedback; 87 | 6. If everything goes well, you should have your changes rebased and merged to the main branch. Otherwise, you will receive comments with adjustments needed before merging. 88 | 89 | > [This](https://cbea.ms/git-commit/) is the commit messaging guidelines you should follow. 90 | 91 | ___ 92 | 93 | Developed with 💚 by [Walmyr](https://walmyr.dev). 94 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress') 2 | const esbuildPreprocessor = require('./cypress/support/esbuild-preprocessor') 3 | const tasks = require('./cypress/support/tasks') 4 | 5 | module.exports = defineConfig({ 6 | projectId: 'vxwq6z', 7 | e2e: { 8 | baseUrl: 'http://localhost/', 9 | setupNodeEvents (on, config) { 10 | esbuildPreprocessor(on) 11 | tasks(on) 12 | return config 13 | } 14 | }, 15 | retries: { 16 | runMode: 2, 17 | openMode: 0 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /cypress.env.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "user_name": "root", 3 | "user_password": "53cR37-p@s5W0rd" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/e2e/api/group/createGroup.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Group', () => { 4 | beforeEach(() => cy.api_deleteGroups()) 5 | 6 | it('creates a group', () => { 7 | const randomUuid = faker.string.uuid() 8 | const group = { 9 | name: `group-${randomUuid}`, 10 | path: randomUuid 11 | } 12 | 13 | cy.api_createGroup(group) 14 | .then(({ status, body }) => { 15 | expect(status).to.equal(201) 16 | expect(body.name).to.equal(group.name) 17 | }) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /cypress/e2e/api/index.cy.js: -------------------------------------------------------------------------------- 1 | // Setup - Sign in (or Sign up) and create an access token 2 | import '../gui/profile/createAccessToken.cy' 3 | 4 | // API tests 5 | import './group/createGroup.cy' 6 | import './project/createProject.cy' 7 | import './user/createUser.cy' 8 | import './user/updateUser.cy' 9 | 10 | // Teardown - Delete access token(s) 11 | import '../gui/profile/deleteAccessTokens.cy' 12 | -------------------------------------------------------------------------------- /cypress/e2e/api/project/createProject.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Project', () => { 4 | beforeEach(() => cy.api_deleteProjects()) 5 | 6 | it('creates a project', () => { 7 | const project = { name: `project-${faker.string.uuid()}` } 8 | 9 | cy.api_createProject(project) 10 | .then(({ status, body }) => { 11 | expect(status).to.equal(201) 12 | expect(body.name).to.equal(project.name) 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /cypress/e2e/api/user/createUser.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('User', () => { 4 | beforeEach(() => cy.deleteAllUsersButRoot()) 5 | 6 | it('creates a user', () => { 7 | const randomName = faker.name.firstName().toLowerCase() 8 | const newUser = { 9 | email: `${randomName}@example.com`, 10 | name: `${randomName} ${faker.name.lastName().toLowerCase()}`, 11 | username: randomName, 12 | password: faker.internet.password() 13 | } 14 | 15 | cy.api_createUser(newUser) 16 | .then(({ status, body }) => { 17 | expect(status).to.equal(201) 18 | expect(body.email).to.equal(newUser.email) 19 | expect(body.name).to.equal(newUser.name) 20 | expect(body.username).to.equal(newUser.username) 21 | }) 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/api/user/updateUser.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('User info', () => { 4 | beforeEach(() => cy.deleteAllUsersButRoot()) 5 | 6 | it('updates user info', () => { 7 | const randomName = faker.name.firstName().toLowerCase() 8 | const newUser = { 9 | email: `${randomName}@example.com`, 10 | name: `${randomName} ${faker.name.lastName().toLowerCase()}`, 11 | username: randomName, 12 | password: faker.internet.password() 13 | } 14 | const website = `https://${randomName}.example.com` 15 | 16 | cy.api_createUser(newUser) 17 | .as('newUser') 18 | .its('status') 19 | .should('be.equal', 201) 20 | cy.get('@newUser') 21 | .then(({ body }) => { 22 | cy.api_updateUserWebsite(body.id, website).then(({ status, body }) => { 23 | expect(status).to.equal(200) 24 | expect(body.username).to.equal(newUser.username) 25 | expect(body.website_url).to.equal(website) 26 | }) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /cypress/e2e/gui/admin/broadcastMessage.cy.js: -------------------------------------------------------------------------------- 1 | describe('Broadcast message', () => { 2 | beforeEach(() => { 3 | cy.api_deleteBroadcastMessages() 4 | cy.sessionLogin() 5 | }) 6 | 7 | it('shows a preview then adds the message', () => { 8 | const broadcastMessage = 'Hello world!' 9 | 10 | // Arrange 11 | cy.visit('admin/broadcast_messages') 12 | 13 | // Act 14 | cy.get('#broadcast_message_message').type(broadcastMessage) 15 | // Intermediate assertion 16 | cy.contains('.broadcast-message-preview', broadcastMessage).should('be.visible') 17 | // Act 18 | cy.contains('Add broadcast message').click() 19 | 20 | // Assert 21 | cy.contains('Broadcast Message was successfully created.').should('be.visible') 22 | cy.contains('table tr', broadcastMessage).should('be.visible') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /cypress/e2e/gui/admin/impersonateUser.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('User impersonation', () => { 4 | const randomName = faker.name.firstName().toLowerCase() 5 | const newUser = { 6 | email: `${randomName}@example.com`, 7 | name: `${randomName} ${faker.name.lastName().toLowerCase()}`, 8 | username: randomName, 9 | password: faker.internet.password(), 10 | skip_confirmation: true 11 | } 12 | 13 | beforeEach(() => { 14 | cy.deleteAllUsersButRoot() 15 | cy.api_createUser(newUser) 16 | cy.sessionLogin() 17 | }) 18 | 19 | it('impersonates and stops impersonating a user', () => { 20 | const { username } = newUser 21 | 22 | // Arrange 23 | cy.visit(`admin/users/${username}`) 24 | 25 | // Act 26 | cy.get('[data-qa-selector="impersonate_user_link"]').click() 27 | 28 | // Assert 29 | cy.url().should('be.equal', Cypress.config('baseUrl')) 30 | cy.contains('.flash-alert', `You are now impersonating ${username}`) 31 | .should('be.visible') 32 | 33 | // Act 34 | cy.get('.qa-user-avatar').click() 35 | 36 | // Assert 37 | cy.get('.dropdown-menu-right ul li.current-user') 38 | .should('contain', newUser.name) 39 | .and('contain', `@${username}`) 40 | 41 | // Act 42 | cy.get('[data-qa-selector="stop_impersonation_link"]').click() 43 | 44 | // Assert 45 | cy.location('pathname').should('be.equal', `/admin/users/${username}`) 46 | cy.contains('.flash-alert', `You are now impersonating ${username}`) 47 | .should('not.exist') 48 | 49 | // Act 50 | cy.get('.qa-user-avatar').click() 51 | 52 | // Assert 53 | cy.get('.dropdown-menu-right ul li.current-user') 54 | .should('contain', 'Administrator') 55 | .and('contain', `@${Cypress.env('user_name')}`) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /cypress/e2e/gui/allButProject.cy.js: -------------------------------------------------------------------------------- 1 | // Setup - Sign in (or Sign up) and create an access token 2 | import '../gui/profile/createAccessToken.cy' 3 | 4 | // GUI tests (all but project) 5 | import './admin/broadcastMessage.cy' 6 | // The below test destroys the user session. The next one recreates it. 7 | import './admin/impersonateUser.cy' 8 | import './group/createGroup.cy' 9 | import './group/createGroupLabel.cy' 10 | import './group/removeGroup.cy' 11 | import './group/subGroup.cy' 12 | import './profile/setStatus.cy' 13 | import './snippets/createSnippet.cy' 14 | import './authentication/loginAsNonDefaultUser.cy' 15 | 16 | // Teardown - Delete access token(s) 17 | import './profile/deleteAccessTokens.cy' 18 | // Leave the logout test to the end since it destroys the user session 19 | import './authentication/logout.cy' 20 | -------------------------------------------------------------------------------- /cypress/e2e/gui/authentication/login.cy.js: -------------------------------------------------------------------------------- 1 | describe('Login', () => { 2 | it('logs in as default user', () => { 3 | /** 4 | * The `sessionLogin` custom cmd uses the `gui_login` cmd, 5 | * which already asserts that the user is logged in, to ensure 6 | * the session is correctly created. 7 | * 8 | * This is why this test has no explicit assertion. 9 | */ 10 | cy.sessionLogin() 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/gui/authentication/loginAsNonDefaultUser.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Login', () => { 4 | const randomName = faker.name.firstName().toLowerCase() 5 | const newUser = { 6 | email: `${randomName}@example.com`, 7 | name: `${randomName} ${faker.name.lastName().toLowerCase()}`, 8 | username: randomName, 9 | password: faker.internet.password(), 10 | skip_confirmation: true 11 | } 12 | 13 | beforeEach(() => { 14 | cy.deleteAllUsersButRoot() 15 | cy.api_createUser(newUser) 16 | }) 17 | 18 | it('logs in as a non-default user', () => { 19 | /** 20 | * The `gui_login` cmd already asserts that the user is logged in, 21 | * to ensure the session is correctly created. 22 | * 23 | * This is why this test has no explicit assertion. 24 | */ 25 | cy.gui_login(newUser.username, newUser.password) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/gui/authentication/logout.cy.js: -------------------------------------------------------------------------------- 1 | describe('Logout', () => { 2 | beforeEach(() => { 3 | cy.sessionLogin() 4 | cy.visit('') 5 | }) 6 | 7 | it('logs out', () => { 8 | cy.gui_logout() 9 | 10 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}users/sign_in`) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/gui/group/createGroup.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Group', () => { 4 | beforeEach(() => { 5 | cy.api_deleteGroups() 6 | cy.sessionLogin() 7 | }) 8 | 9 | it('creates a group', () => { 10 | const group = { 11 | name: `group-${faker.string.uuid()}`, 12 | description: faker.word.words(5) 13 | } 14 | 15 | cy.gui_createPublicGroup(group) 16 | 17 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}${group.name}`) 18 | cy.contains('h1', group.name).should('be.visible') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /cypress/e2e/gui/group/createGroupLabel.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Group Label', () => { 4 | const randomUuid = faker.string.uuid() 5 | const group = { 6 | name: `group-${randomUuid}`, 7 | path: randomUuid, 8 | label: { 9 | title: `label-${faker.word.sample()}` 10 | } 11 | } 12 | 13 | beforeEach(() => { 14 | cy.api_deleteGroups() 15 | cy.sessionLogin() 16 | cy.api_createGroup(group) 17 | cy.visit(group.path) 18 | }) 19 | 20 | it('creates a group label', () => { 21 | cy.gui_createGroupLabel(group, group.label) 22 | 23 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}groups/${group.path}/-/labels`) 24 | cy.contains('.manage-labels-list', group.label.title).should('be.visible') 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/gui/group/removeGroup.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Group', () => { 4 | const randomUuid = faker.string.uuid() 5 | const group = { 6 | name: `group-${randomUuid}`, 7 | path: randomUuid 8 | } 9 | 10 | beforeEach(() => { 11 | // Arrange 12 | cy.api_deleteGroups() 13 | cy.sessionLogin() 14 | cy.api_createGroup(group) 15 | }) 16 | 17 | it('removes a group', () => { 18 | // Act 19 | cy.gui_removeGroup(group) 20 | 21 | // Assert 22 | cy.contains( 23 | '.flash-alert', 24 | `Group '${group.name}' was scheduled for deletion.` 25 | ).should('be.visible') 26 | 27 | // Act 28 | cy.visit('groups') 29 | 30 | // Assert 31 | cy.get('.group-empty-state').should('be.visible') 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /cypress/e2e/gui/group/subGroup.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Sub-group', () => { 4 | const randomUuid = faker.string.uuid() 5 | const group = { 6 | name: `group-${randomUuid}`, 7 | path: randomUuid, 8 | subgroup: { 9 | name: `sub-group-${faker.string.uuid()}` 10 | } 11 | } 12 | 13 | beforeEach(() => { 14 | // Arrange 15 | cy.api_deleteGroups() 16 | cy.sessionLogin() 17 | cy.api_createGroup(group) 18 | .its('body') 19 | .as('groupBody') 20 | }) 21 | 22 | it('creates a sub-group and searches for it', function () { 23 | // Act 24 | cy.gui_createSubgroup(this.groupBody.id, group.subgroup) 25 | 26 | // Assert 27 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}${group.path}/${group.subgroup.name}`) 28 | cy.contains('h1', group.subgroup.name).should('be.visible') 29 | 30 | // Act 31 | cy.visit('dashboard/groups') 32 | cy.get('.qa-groups-filter').type(group.subgroup.name) 33 | 34 | // Assert 35 | cy.contains( 36 | '.qa-groups-list-tree-container', 37 | group.subgroup.name 38 | ).should('be.visible') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /cypress/e2e/gui/profile/createAccessToken.cy.js: -------------------------------------------------------------------------------- 1 | describe('Access Token', () => { 2 | beforeEach(() => cy.sessionLogin()) 3 | 4 | it('creates an access token', () => { 5 | /** 6 | * The `gui_createAccessToken` custom command hides its assertion 7 | * to avoid complexity into the test code. But it does ensure that 8 | * both the success message and token are displayed. 9 | * 10 | * This is why this test has no explicit assertion. 11 | */ 12 | cy.gui_createAccessToken() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /cypress/e2e/gui/profile/deleteAccessTokens.cy.js: -------------------------------------------------------------------------------- 1 | describe("Access Token's clean up", () => { 2 | beforeEach(() => cy.sessionLogin()) 3 | 4 | it('deletes all access tokens', () => { 5 | cy.gui_deleteAccessTokens() 6 | 7 | cy.contains( 8 | '.settings-message', 9 | 'This user has no active Personal Access Tokens' 10 | ).should('be.visible') 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /cypress/e2e/gui/profile/setStatus.cy.js: -------------------------------------------------------------------------------- 1 | describe('Set Status', () => { 2 | beforeEach(() => { 3 | cy.sessionLogin() 4 | cy.visit('') 5 | }) 6 | 7 | it.skip('sets, edits and clears user status', () => { 8 | /** 9 | * All custom commands used in this test already provide assertions 10 | * that the status was correctly set, edited, or deleted. 11 | * 12 | * Thi is only hidden in here to improve test's readability, since 13 | * the commands use other custom commands. 14 | * 15 | * This is why this test has no explicit assertions. 16 | */ 17 | let emojiCode = 'computer' 18 | let statusText = 'Working' 19 | 20 | cy.gui_setStatus(emojiCode, statusText) 21 | 22 | emojiCode = 'island' 23 | statusText = 'Vacationing' 24 | 25 | cy.gui_ediStatus(emojiCode, statusText) 26 | 27 | cy.gui_clearStatus() 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/assignIssue.cy.js: -------------------------------------------------------------------------------- 1 | describe('Issue', () => { 2 | beforeEach(() => { 3 | cy.api_deleteProjects() 4 | cy.sessionLogin() 5 | cy.api_createIssue().as('issue') 6 | cy.api_getAllProjects() 7 | .its('body') 8 | .as('projectsBody') 9 | }) 10 | 11 | it.skip('assigns an issue to yourself', function () { 12 | const { name: projectName } = this.projectsBody[0] 13 | const { iid: issueIid } = this.issue.body 14 | 15 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${issueIid}`) 16 | 17 | cy.contains('assign yourself').click() 18 | 19 | cy.contains('.qa-assignee-block', `@${Cypress.env('user_name')}`) 20 | .should('be.visible') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/closeIssue.cy.js: -------------------------------------------------------------------------------- 1 | describe('Issue', () => { 2 | beforeEach(() => { 3 | cy.api_deleteProjects() 4 | cy.sessionLogin() 5 | cy.api_createIssue().as('issue') 6 | cy.api_getAllProjects() 7 | .its('body') 8 | .as('projectsBody') 9 | }) 10 | 11 | it('closes an issue', function () { 12 | const { name: projectName } = this.projectsBody[0] 13 | const { iid: issueIid } = this.issue.body 14 | 15 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${issueIid}`) 16 | 17 | cy.get('.d-none.btn-close').click() 18 | 19 | cy.contains('.status-box-issue-closed', 'Closed') 20 | .should('be.visible') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/closeIssueQuickAction.cy.js: -------------------------------------------------------------------------------- 1 | describe('Issue - Quick action', () => { 2 | beforeEach(() => { 3 | cy.api_deleteProjects() 4 | cy.sessionLogin() 5 | cy.api_createIssue().as('issue') 6 | cy.api_getAllProjects() 7 | .its('body') 8 | .as('projectsBody') 9 | }) 10 | 11 | it.skip('closes an issue using a quick action', function () { 12 | const { name: projectName } = this.projectsBody[0] 13 | const { iid: issueIid } = this.issue.body 14 | 15 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${issueIid}`) 16 | 17 | cy.gui_commentOnIssue('/close ') 18 | 19 | cy.contains('Closed this issue') 20 | .should('be.visible') 21 | 22 | cy.reload() 23 | 24 | cy.contains('.status-box-issue-closed', 'Closed') 25 | .should('be.visible') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/commentOnIssue.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Issue', () => { 4 | beforeEach(() => { 5 | cy.api_deleteProjects() 6 | cy.sessionLogin() 7 | cy.api_createIssue().as('issue') 8 | cy.api_getAllProjects() 9 | .its('body') 10 | .as('projectsBody') 11 | }) 12 | 13 | it('comments on an issue', function () { 14 | const comment = faker.word.words(3) 15 | const { name: projectName } = this.projectsBody[0] 16 | const { iid: issueIid } = this.issue.body 17 | 18 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${issueIid}`) 19 | 20 | cy.gui_commentOnIssue(comment) 21 | 22 | cy.contains('.qa-noteable-note-item', comment) 23 | .should('be.visible') 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/createIssue.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Issue', () => { 4 | const project = { 5 | name: `project-${faker.string.uuid()}`, 6 | description: faker.word.words(5), 7 | issue: { 8 | title: `issue-${faker.string.uuid()}`, 9 | description: faker.word.words(3) 10 | } 11 | } 12 | 13 | beforeEach(() => { 14 | cy.api_deleteProjects() 15 | cy.sessionLogin() 16 | cy.api_createProject(project) 17 | }) 18 | 19 | it('creates an issue', () => { 20 | cy.gui_createIssue(project, project.issue) 21 | 22 | cy.get('.issue-details') 23 | .should('contain', project.issue.title) 24 | .and('contain', project.issue.description) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/createNewFile.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('File', () => { 4 | const project = { 5 | name: `project-${faker.string.uuid()}`, 6 | file: { 7 | name: `${faker.word.sample()}.txt`, 8 | content: faker.word.words(10) 9 | } 10 | } 11 | 12 | beforeEach(() => { 13 | cy.api_deleteProjects() 14 | cy.sessionLogin() 15 | cy.api_createProject(project) 16 | }) 17 | 18 | it.skip('creates a new file', () => { 19 | cy.visit(`${Cypress.env('user_name')}/${project.name}/new/master`) 20 | 21 | cy.gui_createFile(project.file) 22 | 23 | cy.contains('The file has been successfully created.').should('be.visible') 24 | cy.contains(project.file.name).should('be.visible') 25 | cy.contains(project.file.content).should('be.visible') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/createProject.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Project', () => { 4 | beforeEach(() => { 5 | cy.api_deleteProjects() 6 | cy.sessionLogin() 7 | }) 8 | 9 | it('creates a project', () => { 10 | const project = { 11 | name: `project-${faker.string.uuid()}`, 12 | description: faker.word.words(5) 13 | } 14 | 15 | cy.gui_createProject(project) 16 | 17 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}${Cypress.env('user_name')}/${project.name}`) 18 | cy.contains('h1', project.name).should('be.visible') 19 | cy.contains('p', project.description).should('be.visible') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/createProjectMilestone.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Projet Milestone', () => { 4 | const project = { 5 | name: `project-${faker.string.uuid()}`, 6 | milestone: { 7 | title: `milestone-${faker.string.uuid()}` 8 | } 9 | } 10 | 11 | beforeEach(() => { 12 | cy.api_deleteProjects() 13 | cy.sessionLogin() 14 | cy.api_createProject(project) 15 | }) 16 | 17 | it('creates a project milestone', () => { 18 | cy.gui_createProjectMilestone(project, project.milestone) 19 | 20 | cy.contains('.milestone-detail h2', project.milestone.title).should('be.visible') 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/createWiki.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Wiki', () => { 4 | const project = { name: `project-${faker.string.uuid()}` } 5 | 6 | beforeEach(() => { 7 | cy.api_deleteProjects() 8 | cy.sessionLogin() 9 | cy.api_createProject(project) 10 | }) 11 | 12 | it('creates a wiki', () => { 13 | const wikiContent = faker.word.words(4) 14 | 15 | cy.visit(`${Cypress.env('user_name')}/${project.name}/wikis/home?view=create`) 16 | 17 | cy.get('.qa-wiki-content-textarea').type(wikiContent) 18 | cy.contains('Create page').click() 19 | 20 | cy.url().should('be.equal', `${Cypress.config('baseUrl')}${Cypress.env('user_name')}/${project.name}/wikis/home`) 21 | cy.contains('[data-qa-selector="wiki_page_content"]', wikiContent).should('be.visible') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/issueBoard.cy.js: -------------------------------------------------------------------------------- 1 | describe('Issue board', () => { 2 | beforeEach(() => { 3 | cy.api_deleteProjects() 4 | cy.sessionLogin() 5 | }) 6 | 7 | it('shows an open issue on the issue board, closes it, and shows it closed', () => { 8 | cy.api_createIssue().as('issue') 9 | cy.api_getAllProjects() 10 | .then(function ({ body }) { 11 | const { title, iid } = this.issue.body 12 | const { name: projectName } = body[0] 13 | 14 | cy.intercept('PUT', `${Cypress.env('user_name')}/${projectName}/issues/**`) 15 | .as('closeIssueRequest') 16 | 17 | cy.visit(`${Cypress.env('user_name')}/${projectName}/-/boards`) 18 | 19 | cy.contains('[data-board-type="backlog"] [data-qa-selector="board_card"]', title) 20 | .should('be.visible') 21 | 22 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${iid}`) 23 | cy.get('.d-none.btn-close').click() 24 | cy.wait('@closeIssueRequest') 25 | 26 | cy.visit(`${Cypress.env('user_name')}/${projectName}/-/boards`) 27 | 28 | cy.contains('[data-board-type="closed"] [data-qa-selector="board_card"]', title) 29 | .should('be.visible') 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/issueMilestone.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Issue milestone', () => { 4 | const milestone = { title: `milestone-${faker.word.sample()}` } 5 | 6 | beforeEach(() => { 7 | cy.api_deleteProjects() 8 | cy.sessionLogin() 9 | cy.api_createIssue().as('issue') 10 | cy.api_getAllProjects() 11 | .then(function ({ body }) { 12 | const project = body[0] 13 | const issueIid = this.issue.body.iid 14 | 15 | cy.api_createProjectMilestone(project.id, milestone) 16 | cy.visit(`${Cypress.env('user_name')}/${project.name}/issues/${issueIid}`) 17 | }) 18 | }) 19 | 20 | it('adds a milestone to an issue', () => { 21 | cy.gui_addMilestoneOnIssue(milestone) 22 | 23 | cy.get('.block.milestone').should('contain', milestone.title) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/labelAnIssue.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Issue label', () => { 4 | const label = { 5 | name: `label-${faker.word.sample()}`, 6 | color: '#ffaabb' 7 | } 8 | 9 | beforeEach(() => { 10 | cy.api_deleteProjects() 11 | cy.sessionLogin() 12 | cy.api_createIssue().as('issue') 13 | cy.api_getAllProjects() 14 | .then(function ({ body }) { 15 | const project = body[0] 16 | const issueIid = this.issue.body.iid 17 | 18 | cy.api_createProjectLabel(project.id, label) 19 | cy.visit(`${Cypress.env('user_name')}/${project.name}/issues/${issueIid}`) 20 | }) 21 | }) 22 | 23 | it('labels an issue', () => { 24 | cy.gui_labelIssueWith(label) 25 | 26 | cy.contains('.qa-labels-block', label.name).should('be.visible') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/multipleUsersInAnProject.cy.js: -------------------------------------------------------------------------------- 1 | const newUser = require('../../../fixtures/sampleUser') 2 | const { username: newUserName, password: newUserPassword } = newUser 3 | const defaultUser = Cypress.env('user_name') 4 | 5 | describe('Project with multiple users', () => { 6 | beforeEach(() => { 7 | cy.log('--- Pre-conditions ---') 8 | cy.log('1. Delete all users and projects to start in a clean state') 9 | cy.deleteAllUsersButRoot() 10 | cy.api_deleteProjects() 11 | 12 | cy.log('2. Create a brand new user') 13 | cy.api_createUser(newUser) 14 | 15 | cy.log(`3. Create a new issue (and by consequence a new project) for the ${defaultUser} user`) 16 | cy.api_createIssue() 17 | .its('body.iid') 18 | .as('issueIid') 19 | 20 | cy.log(`4. Sign in as ${defaultUser}`) 21 | cy.signInAsDefaultUser() 22 | 23 | cy.log(`5. Add the new user to the ${defaultUser} user's project`) 24 | cy.api_getAllProjects().as('projects') 25 | cy.get('@projects') 26 | .its('body[0].name') 27 | .as('projectName') 28 | .then(projectName => cy.gui_addUserToProject(newUser, projectName)) 29 | cy.log('--- End of pre-conditions ---') 30 | }) 31 | 32 | it('users reply to each other in an issue', () => { 33 | cy.log(`1. Visit the issue as ${defaultUser} and comment on it`) 34 | cy.visitIssue() 35 | cy.gui_commentOnIssue(`Hi @${newUserName}, what do you think?`) 36 | 37 | cy.log(`2. Sign in as ${newUserName}, visit the issue, and reply to the comment`) 38 | cy.signInAsNewUser() 39 | cy.visitIssue() 40 | cy.assertCommentIsVisible('what do you think?') 41 | cy.gui_commentOnIssue(`Hey, @${defaultUser}, it looks good to me.`) 42 | 43 | cy.log(`3. Sign in as ${defaultUser}, visit the issue, and reply to the new comment`) 44 | cy.signInAsDefaultUser() 45 | cy.visitIssue() 46 | cy.assertCommentIsVisible('looks good to me.') 47 | cy.gui_commentOnIssue('Great, thanks!') 48 | 49 | cy.log(`4. Sign in as ${newUserName}, visit the issue, and reply to the newest comment`) 50 | cy.signInAsNewUser() 51 | cy.visitIssue() 52 | cy.assertCommentIsVisible('Great, thanks!') 53 | cy.gui_commentOnIssue('You are welcome!') 54 | 55 | cy.log(`5. Sign in as ${defaultUser}, visit the issue, and see the last comment`) 56 | cy.signInAsDefaultUser() 57 | cy.visitIssue() 58 | cy.assertCommentIsVisible('You are welcome!') 59 | }) 60 | }) 61 | 62 | Cypress.Commands.add('visitIssue', function () { 63 | const { projectName, issueIid } = this 64 | cy.visit(`${defaultUser}/${projectName}/issues/${issueIid}`) 65 | }) 66 | 67 | Cypress.Commands.add('signInAsDefaultUser', () => { 68 | cy.sessionLogin() 69 | }) 70 | 71 | Cypress.Commands.add('signInAsNewUser', () => { 72 | cy.sessionLogin(newUserName, newUserPassword) 73 | }) 74 | 75 | Cypress.Commands.add('assertCommentIsVisible', comment => { 76 | cy.contains('.timeline-content', comment).should('be.visible') 77 | }) 78 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/projectButIssue.cy.js: -------------------------------------------------------------------------------- 1 | // Setup - Sign in (or Sign up) and create an access token 2 | import '../profile/createAccessToken.cy.js' 3 | 4 | // GUI tests (project, but issue) 5 | import './createNewFile.cy.js' 6 | import './createProject.cy.js' 7 | import './createProjectMilestone.cy.js' 8 | import './createWiki.cy.js' 9 | import './starProject.cy.js' 10 | 11 | // Teardown - Delete access token(s) 12 | import '../profile/deleteAccessTokens.cy.js' 13 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/projectIssue.cy.js: -------------------------------------------------------------------------------- 1 | // Setup - Sign in (or Sign up) and create an access token 2 | import '../profile/createAccessToken.cy.js' 3 | 4 | // GUI tests (project|issue) 5 | import './assignIssue.cy.js' 6 | import './closeIssue.cy.js' 7 | import './closeIssueQuickAction.cy.js' 8 | import './commentOnIssue.cy.js' 9 | import './createIssue.cy.js' 10 | import './issueBoard.cy.js' 11 | import './issueMilestone.cy.js' 12 | import './labelAnIssue.cy.js' 13 | import './reopenClosedIssue.cy.js' 14 | import './multipleUsersInAnProject.cy.js' 15 | 16 | // Teardown - Delete access token(s) 17 | import '../profile/deleteAccessTokens.cy.js' 18 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/reopenClosedIssue.cy.js: -------------------------------------------------------------------------------- 1 | describe('Closed issue', () => { 2 | beforeEach(() => { 3 | cy.api_deleteProjects() 4 | cy.sessionLogin() 5 | cy.api_createIssue().as('issue') 6 | cy.api_getAllProjects() 7 | .then(function ({ body }) { 8 | const { name: projectName } = body[0] 9 | const { iid: issueIid } = this.issue.body 10 | 11 | cy.visit(`${Cypress.env('user_name')}/${projectName}/issues/${issueIid}`) 12 | cy.get('.d-none.btn-close').click() 13 | }) 14 | }) 15 | 16 | it('reopens a closed issue', () => { 17 | cy.get('[data-qa-selector="reopen_issue_button"]').click() 18 | 19 | cy.contains('.status-box-open', 'Open').should('be.visible') 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /cypress/e2e/gui/project/starProject.cy.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | describe('Star project', () => { 4 | const project = { 5 | name: `project-${faker.string.uuid()}`, 6 | description: faker.word.words(5), 7 | issue: { 8 | title: `issue-${faker.string.uuid()}`, 9 | description: faker.word.words(3) 10 | } 11 | } 12 | 13 | beforeEach(() => { 14 | cy.api_deleteProjects() 15 | cy.sessionLogin() 16 | cy.api_createProject(project) 17 | }) 18 | 19 | it('stars a project', () => { 20 | cy.intercept( 21 | 'POST', 22 | `${Cypress.env('user_name')}/${project.name}/toggle_star.json` 23 | ).as('starRequest') 24 | 25 | cy.visit(`${Cypress.config('baseUrl')}${Cypress.env('user_name')}/${project.name}`) 26 | 27 | cy.get('.star-btn').click() 28 | cy.wait('@starRequest') 29 | .its('response.statusCode') 30 | .should('equal', 200) 31 | 32 | cy.visit('dashboard/projects/starred') 33 | 34 | cy.get('[data-qa-selector="projects_list"]') 35 | .find(`ul li:contains(${project.name})`) 36 | .should('be.visible') 37 | 38 | cy.visit('explore/projects/starred') 39 | 40 | cy.get('[data-qa-selector="projects_list"]') 41 | .find(`ul li:contains(${project.name})`) 42 | .should('be.visible') 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /cypress/e2e/gui/snippets/createSnippet.cy.js: -------------------------------------------------------------------------------- 1 | describe('Snippet', () => { 2 | beforeEach(() => { 3 | cy.api_deleteSnippets() 4 | cy.sessionLogin() 5 | }) 6 | 7 | it('creates a public snippet', () => { 8 | const snippetObj = { 9 | title: 'JS Hello, World!', 10 | description: '"Hello, World" example in JavaScript', 11 | visibility: 'public', 12 | snippet: 'console.log("Hello, World!")' 13 | } 14 | const { title, description, snippet } = snippetObj 15 | 16 | cy.visit('snippets/new') 17 | 18 | cy.gui_createSnippet(snippetObj) 19 | 20 | cy.contains('h2', title).should('be.visible') 21 | cy.contains('p', description).should('be.visible') 22 | cy.contains('pre code span', snippet).should('be.visible') 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /cypress/fixtures/sampleUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "john-doe@example.com", 3 | "name": "John Doe", 4 | "username": "johndoe", 5 | "password": "53cR37-p@s5W0rd", 6 | "skip_confirmation": true 7 | } 8 | -------------------------------------------------------------------------------- /cypress/support/commands/api_commands.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | let accessToken 4 | 5 | /** 6 | * Ideally, the `setAccessTokenIfNotYetSet` function should be called just once, 7 | * right before the definition of all custom commands. 8 | * However, `cy.task` can only be called from within a test, which means that 9 | * calling it from outside of a test would result in a 10 | * `Cannot call cy.task() outside a running test.` error. 11 | * 12 | * This is why `setAccessTokenIfNotYetSet` is defined here as a function 13 | * that can be called by custom commands (or tests.) 14 | * 15 | * Since custom commands are called by test, calling the 16 | * `setAccessTokenIfNotYetSet` function inside them is like calling it 17 | * from inside the test that uses the command, making it a valid call. 18 | * 19 | * This is why every `api_*` command needs to call `setAccessTokenIfNotYetSet` 20 | * at the begining of their body. 21 | */ 22 | const setAccessTokenIfNotYetSet = () => { 23 | if (!accessToken) { 24 | cy.task('getToken') 25 | .then(token => { 26 | accessToken = token 27 | }) 28 | } 29 | } 30 | 31 | Cypress.Commands.add('api_createGroup', ({ name, path }) => { 32 | setAccessTokenIfNotYetSet() 33 | cy.request({ 34 | method: 'POST', 35 | url: '/api/v4/groups', 36 | headers: { 'Private-Token': accessToken }, 37 | body: { name, path } 38 | }) 39 | }) 40 | 41 | Cypress.Commands.add('api_getAllGroups', () => { 42 | setAccessTokenIfNotYetSet() 43 | cy.request({ 44 | method: 'GET', 45 | url: '/api/v4/groups', 46 | headers: { 'Private-Token': accessToken } 47 | }) 48 | }) 49 | 50 | Cypress.Commands.add('api_deleteGroups', () => { 51 | setAccessTokenIfNotYetSet() 52 | cy.api_getAllGroups() 53 | .its('body') 54 | .each(({ id }) => { 55 | cy.request({ 56 | method: 'DELETE', 57 | url: `/api/v4/groups/${id}`, 58 | headers: { 'Private-Token': accessToken } 59 | }) 60 | }) 61 | }) 62 | 63 | Cypress.Commands.add('api_createProject', ({ name }) => { 64 | setAccessTokenIfNotYetSet() 65 | cy.request({ 66 | method: 'POST', 67 | url: '/api/v4/projects', 68 | headers: { 'Private-Token': accessToken }, 69 | body: { name } 70 | }) 71 | }) 72 | 73 | Cypress.Commands.add('api_getAllProjects', () => { 74 | setAccessTokenIfNotYetSet() 75 | cy.request({ 76 | method: 'GET', 77 | url: '/api/v4/projects', 78 | headers: { 'Private-Token': accessToken } 79 | }) 80 | }) 81 | 82 | Cypress.Commands.add('api_deleteProjects', () => { 83 | setAccessTokenIfNotYetSet() 84 | cy.api_getAllProjects() 85 | .its('body') 86 | .each(({ id }) => { 87 | cy.request({ 88 | method: 'DELETE', 89 | url: `/api/v4/projects/${id}`, 90 | headers: { 'Private-Token': accessToken } 91 | }) 92 | }) 93 | }) 94 | 95 | Cypress.Commands.add('api_createIssue', () => { 96 | setAccessTokenIfNotYetSet() 97 | cy.api_createProject({ name: `project-${faker.string.uuid()}` }) 98 | .then(({ body }) => { 99 | cy.request({ 100 | method: 'POST', 101 | url: `/api/v4/projects/${body.id}/issues`, 102 | headers: { 'Private-Token': accessToken }, 103 | body: { title: `issue-${faker.string.uuid()}` } 104 | }) 105 | }) 106 | }) 107 | 108 | Cypress.Commands.add('api_createProjectLabel', (projectId, label) => { 109 | setAccessTokenIfNotYetSet() 110 | cy.request({ 111 | method: 'POST', 112 | url: `/api/v4/projects/${projectId}/labels`, 113 | headers: { 'Private-Token': accessToken }, 114 | body: { 115 | name: label.name, 116 | color: label.color 117 | } 118 | }) 119 | }) 120 | 121 | Cypress.Commands.add('api_createProjectMilestone', (projectId, milestone) => { 122 | setAccessTokenIfNotYetSet() 123 | cy.request({ 124 | method: 'POST', 125 | url: `/api/v4/projects/${projectId}/milestones`, 126 | headers: { 'Private-Token': accessToken }, 127 | body: { title: milestone.title } 128 | }) 129 | }) 130 | 131 | Cypress.Commands.add('api_createUser', user => { 132 | setAccessTokenIfNotYetSet() 133 | 134 | let skipConfirmation = false 135 | 136 | if (Object.prototype.hasOwnProperty.call(user, 'skip_confirmation')) { 137 | skipConfirmation = user.skip_confirmation 138 | } 139 | 140 | cy.request({ 141 | method: 'POST', 142 | url: '/api/v4/users', 143 | headers: { 'Private-Token': accessToken }, 144 | body: { 145 | email: user.email, 146 | name: user.name, 147 | username: user.username, 148 | password: user.password, 149 | skip_confirmation: skipConfirmation 150 | } 151 | }) 152 | }) 153 | 154 | Cypress.Commands.add('api_getAllUsers', () => { 155 | setAccessTokenIfNotYetSet() 156 | cy.request({ 157 | method: 'GET', 158 | url: '/api/v4/users', 159 | headers: { 'Private-Token': accessToken } 160 | }) 161 | }) 162 | 163 | Cypress.Commands.add('api_deleteUser', userId => { 164 | setAccessTokenIfNotYetSet() 165 | cy.request({ 166 | method: 'DELETE', 167 | url: `/api/v4/users/${userId}`, 168 | headers: { 'Private-Token': accessToken } 169 | }) 170 | }) 171 | 172 | Cypress.Commands.add('deleteAllUsersButRoot', () => { 173 | setAccessTokenIfNotYetSet() 174 | cy.api_getAllUsers() 175 | .its('body') 176 | .each(({ username, id }) => { 177 | if (username !== 'root') { 178 | cy.api_deleteUser(id) 179 | .its('status') 180 | .should('equal', 204) 181 | } 182 | }) 183 | }) 184 | 185 | Cypress.Commands.add('api_updateUserWebsite', (userId, website) => { 186 | setAccessTokenIfNotYetSet() 187 | cy.request({ 188 | method: 'PUT', 189 | url: `/api/v4/users/${userId}`, 190 | headers: { 'Private-Token': accessToken }, 191 | body: { website_url: website } 192 | }) 193 | }) 194 | 195 | Cypress.Commands.add('api_getAllBroadcastMessages', () => { 196 | setAccessTokenIfNotYetSet() 197 | cy.request({ 198 | method: 'GET', 199 | url: '/api/v4/broadcast_messages', 200 | headers: { 'Private-Token': accessToken } 201 | }) 202 | }) 203 | 204 | Cypress.Commands.add('api_deleteBroadcastMessages', () => { 205 | setAccessTokenIfNotYetSet() 206 | cy.api_getAllBroadcastMessages() 207 | .its('body') 208 | .each(({ id }) => { 209 | cy.request({ 210 | method: 'DELETE', 211 | url: `/api/v4/broadcast_messages/${id}`, 212 | headers: { 'Private-Token': accessToken } 213 | }) 214 | }) 215 | }) 216 | 217 | Cypress.Commands.add('api_getAllSnippets', () => { 218 | setAccessTokenIfNotYetSet() 219 | cy.request({ 220 | method: 'GET', 221 | url: '/api/v4/snippets', 222 | headers: { 'Private-Token': accessToken } 223 | }) 224 | }) 225 | 226 | Cypress.Commands.add('api_deleteSnippets', () => { 227 | setAccessTokenIfNotYetSet() 228 | cy.api_getAllSnippets() 229 | .its('body') 230 | .each(({ id }) => { 231 | cy.request({ 232 | method: 'DELETE', 233 | url: `/api/v4/snippets/${id}`, 234 | headers: { 'Private-Token': accessToken } 235 | }) 236 | }) 237 | }) 238 | -------------------------------------------------------------------------------- /cypress/support/commands/gui_commands.js: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker/locale/en' 2 | 3 | Cypress.Commands.add('gui_login', ( 4 | username = Cypress.env('user_name'), 5 | password = Cypress.env('user_password') 6 | ) => { 7 | cy.visit('users/sign_in') 8 | 9 | cy.get('[data-qa-selector="login_field"]').type(username) 10 | cy.get('[data-qa-selector="password_field"]').type(password, { log: false }) 11 | cy.get('[data-qa-selector="sign_in_button"]').click() 12 | 13 | cy.get('.qa-user-avatar').should('exist') 14 | }) 15 | 16 | Cypress.Commands.add('gui_login_or_signup_and_login', ( 17 | username = Cypress.env('user_name'), 18 | password = Cypress.env('user_password') 19 | ) => { 20 | cy.visit('') 21 | 22 | cy.url().then(url => { 23 | if (url.includes('/users/password/edit?reset_password_token=')) { 24 | cy.signup(password) 25 | } 26 | }) 27 | 28 | cy.gui_login(username, password) 29 | }) 30 | 31 | Cypress.Commands.add('signup', (password = Cypress.env('user_password')) => { 32 | cy.get('[data-qa-selector="password_field"]').type(password, { log: false }) 33 | cy.get('[data-qa-selector="password_confirmation_field"]').type(password, { log: false }) 34 | cy.get('[data-qa-selector="change_password_button"]').click() 35 | }) 36 | 37 | Cypress.Commands.add('gui_createAccessToken', (name = faker.string.uuid()) => { 38 | cy.visit('profile/personal_access_tokens') 39 | 40 | cy.get('.qa-personal-access-token-name-field').type(name) 41 | cy.get('.qa-api-radio').check() 42 | cy.get('.qa-create-token-button').click() 43 | 44 | cy.contains('Your new personal access token has been created.') 45 | .should('be.visible') 46 | cy.get('.qa-created-personal-access-token') 47 | .should('be.visible') 48 | .then(($field) => { 49 | const token = $field[0].value 50 | cy.task('saveToken', token) 51 | }) 52 | }) 53 | 54 | Cypress.Commands.add('gui_deleteAccessTokens', () => { 55 | cy.visit('profile/personal_access_tokens') 56 | 57 | cy.get('body').then($body => { 58 | if ($body.find('.settings-message:contains(This user has no active Personal Access Tokens.)').length) { 59 | cy.log('no active tokens were found.') 60 | return 61 | } 62 | cy.get('.active-tokens tbody tr') 63 | .its('length') 64 | .then(numberOfActiveTokens => { 65 | Cypress._.times(numberOfActiveTokens, () => { 66 | cy.get('.qa-revoke-button') 67 | .eq(0) 68 | .click() 69 | }) 70 | }) 71 | }) 72 | }) 73 | 74 | Cypress.Commands.add('gui_createProject', project => { 75 | cy.visit('projects/new') 76 | 77 | cy.get('#project_name').type(project.name) 78 | cy.get('#project_description').type(project.description) 79 | cy.get('.qa-initialize-with-readme-checkbox').check() 80 | cy.contains('Create project').click() 81 | }) 82 | 83 | Cypress.Commands.add('gui_createIssue', (project, issue) => { 84 | cy.visit(`${Cypress.env('user_name')}/${project.name}/issues/new`) 85 | 86 | cy.get('.qa-issuable-form-title').type(issue.title) 87 | cy.get('.qa-issuable-form-description').type(issue.description) 88 | cy.contains('Submit issue').click() 89 | }) 90 | 91 | Cypress.Commands.add('gui_createPublicGroup', group => { 92 | cy.visit('groups/new') 93 | 94 | cy.get('#group_name').type(group.name) 95 | cy.get('#group_description').type(group.description) 96 | cy.get('#group_visibility_level_20').check() 97 | cy.contains('Create group').click() 98 | }) 99 | 100 | Cypress.Commands.add('gui_createSubgroup', (groupId, subgroup) => { 101 | cy.visit(`groups/new?parent_id=${groupId}`) 102 | 103 | cy.get('#group_name').type(subgroup.name) 104 | cy.contains('Create group').click() 105 | }) 106 | 107 | Cypress.Commands.add('gui_createGroupLabel', (group, label) => { 108 | cy.visit(`groups/${group.path}/-/labels/new`) 109 | 110 | cy.get('.qa-label-title').type(label.title) 111 | cy.contains('Create label').click() 112 | }) 113 | 114 | Cypress.Commands.add('gui_removeGroup', ({ path }) => { 115 | /** 116 | * The test that uses this command was flaky when run on CI. 117 | * To avoid flakiness, the following was implemented. 118 | * 119 | * Failure example: 120 | * https://github.com/wlsf82/gitlab-cypress/actions/ 121 | * runs/4463324752/jobs/7838446252 122 | * 123 | * Reference: 124 | * https://docs.cypress.io/api/events/catalog-of-events 125 | * #To-catch-a-single-uncaught-exception 126 | */ 127 | cy.on('uncaught:exception', () => { 128 | // return false to prevent the error from failing this test 129 | return false 130 | }) 131 | 132 | cy.visit(`groups/${path}/-/edit`) 133 | 134 | cy.contains('h4', 'Path, transfer, remove') 135 | .next() 136 | .click() 137 | cy.get('input[value="Remove group"]') 138 | .should('be.visible') 139 | .click() 140 | cy.get('.qa-confirm-input') 141 | .type(path) 142 | cy.get('.qa-confirm-button').click() 143 | }) 144 | 145 | Cypress.Commands.add('gui_createProjectMilestone', (project, milestone) => { 146 | cy.visit(`${Cypress.env('user_name')}/${project.name}/-/milestones/new`) 147 | 148 | cy.get('.qa-milestone-title').type(milestone.title) 149 | cy.get('.qa-milestone-create-button').click() 150 | }) 151 | 152 | Cypress.Commands.add('gui_labelIssueWith', label => { 153 | cy.get('.qa-edit-link-labels').click() 154 | cy.contains(label.name).click() 155 | cy.get('body').click() 156 | }) 157 | 158 | Cypress.Commands.add('gui_commentOnIssue', comment => { 159 | cy.get('.qa-comment-input').type(comment) 160 | cy.get('.qa-comment-button').click() 161 | }) 162 | 163 | Cypress.Commands.add('gui_logout', () => { 164 | cy.get('.qa-user-avatar').click() 165 | cy.contains('Sign out').click() 166 | }) 167 | 168 | Cypress.Commands.add('gui_addMilestoneOnIssue', milestone => { 169 | cy.get('.block.milestone .edit-link').click() 170 | cy.contains(milestone.title).click() 171 | }) 172 | 173 | Cypress.Commands.add('gui_createFile', file => { 174 | cy.get('#file_name').type(file.name) 175 | cy.get('#editor').type(file.content) 176 | cy.get('.qa-commit-button').click() 177 | }) 178 | 179 | Cypress.Commands.add('gui_addUserToProject', (user, project) => { 180 | const { username } = user 181 | 182 | cy.intercept('GET', `autocomplete/users.json?search=@${username}&active=true`) 183 | .as('getUser') 184 | 185 | cy.visit(`${Cypress.env('user_name')}/${project}/-/project_members`) 186 | cy.contains('label', 'GitLab member or Email address') 187 | .next() 188 | .type(`@${username}`) 189 | cy.wait('@getUser') 190 | .its('response.statusCode') 191 | .should('be.oneOf', [200, 304]) 192 | cy.contains('li', `@${username}`) 193 | .as('userListItem') 194 | .should('be.visible') 195 | cy.get('@userListItem') 196 | .click() 197 | cy.get('.qa-add-member-button').click() 198 | cy.contains('.flash-notice', 'Users were successfully added.') 199 | .should('be.visible') 200 | cy.contains('.qa-members-list', `@${username}`) 201 | .should('be.visible') 202 | }) 203 | 204 | Cypress.Commands.add('gui_createSnippet', snippetObj => { 205 | const { title, description, visibility, snippet } = snippetObj 206 | 207 | cy.get('.qa-snippet-title').type(title) 208 | cy.get('.qa-issuable-form-description').type(description) 209 | cy.get(`[data-qa-selector="${visibility}_radio"]`).check() 210 | cy.get('#editor .ace_content').type(snippet) 211 | cy.get('.qa-create-snippet-button').click() 212 | }) 213 | 214 | Cypress.Commands.add('gui_setStatus', (emojiCode, statusText) => { 215 | cy.openStatusModal() 216 | .as('statusModal') 217 | .selectEmojiAndStatusText(emojiCode, statusText) 218 | cy.get('@statusModal') 219 | .find('button:contains(Set status)') 220 | .click() 221 | cy.assertStatus(statusText) 222 | }) 223 | 224 | Cypress.Commands.add('gui_ediStatus', (emojiCode, statusText) => { 225 | cy.gui_setStatus(emojiCode, statusText) 226 | }) 227 | 228 | Cypress.Commands.add('gui_clearStatus', () => { 229 | cy.openStatusModal() 230 | .find('button:contains(Remove status)') 231 | .click() 232 | cy.get('.qa-user-avatar') 233 | .should('be.visible') 234 | .click() 235 | cy.get('.dropdown-menu.show').should('be.visible') 236 | cy.get('.dropdown-menu .user-status') 237 | .should('not.exist') 238 | cy.get('.qa-user-avatar').click() 239 | }) 240 | 241 | /** 242 | * Custom commands defined here without the `gui_` prefix are only 243 | * used by other custom commands, not directly by tests. 244 | * 245 | * This is a project's convention. 246 | */ 247 | 248 | Cypress.Commands.add('selectEmojiAndStatusText', { prevSubject: true }, ( 249 | subject, 250 | emojiCode, 251 | statusText 252 | ) => { 253 | subject.find('[aria-label="Add status emoji"]') 254 | .click() 255 | cy.get('[name="emoji-menu-search"]') 256 | .should('be.visible') 257 | .type(` ${emojiCode}`) 258 | cy.contains('.emoji-search-title', 'Search results') 259 | .next() 260 | .find('li') 261 | .first() 262 | .click() 263 | cy.get('input[placeholder="What\'s your status?"]') 264 | .clear() 265 | .type(statusText) 266 | }) 267 | 268 | Cypress.Commands.add('openStatusModal', () => { 269 | cy.get('.qa-user-avatar') 270 | .click() 271 | cy.contains('button', ' status').click() 272 | cy.get('#set-user-status-modal___BV_modal_content_') 273 | .should('be.visible') 274 | }) 275 | 276 | Cypress.Commands.add('assertStatus', statusText => { 277 | cy.get('.qa-user-avatar') 278 | .as('avatar') 279 | .click() 280 | cy.get('.dropdown-menu .user-status') 281 | .should('contain', statusText) 282 | cy.get('.qa-user-avatar').click() 283 | }) 284 | -------------------------------------------------------------------------------- /cypress/support/commands/session_login.js: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('sessionLogin', ( 2 | user = Cypress.env('user_name'), 3 | password = Cypress.env('user_password') 4 | ) => { 5 | const login = () => cy.gui_login_or_signup_and_login(user, password) 6 | 7 | const validate = () => { 8 | cy.visit('') 9 | cy.location('pathname', { timeout: 1000 }) 10 | .should('not.eq', '/users/sign_in') 11 | } 12 | 13 | const options = { 14 | cacheAcrossSpecs: true, 15 | validate 16 | } 17 | 18 | /** 19 | * @param user string - the id of the session. If the id changes, a new 20 | * session is created. 21 | * @param login function - the function that creates the session. 22 | * @param options object - an object to add certain characteristics to the 23 | * session, such as sharing the cached session across specs (test files), 24 | * and a way to validate if the session is still valid (validate function). 25 | * 26 | * For more details, visit https://docs.cypress.io/api/commands/session 27 | */ 28 | cy.session(user, login, options) 29 | }) 30 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import './commands/api_commands' 2 | import './commands/gui_commands' 3 | import './commands/session_login' 4 | -------------------------------------------------------------------------------- /cypress/support/esbuild-preprocessor.js: -------------------------------------------------------------------------------- 1 | const { NodeGlobalsPolyfillPlugin } = require('@esbuild-plugins/node-globals-polyfill') 2 | const { NodeModulesPolyfillPlugin } = require('@esbuild-plugins/node-modules-polyfill') 3 | const createBundler = require('@bahmutov/cypress-esbuild-preprocessor') 4 | 5 | module.exports = function tasks (on) { 6 | on( 7 | 'file:preprocessor', 8 | createBundler({ 9 | plugins: [ 10 | NodeModulesPolyfillPlugin(), 11 | NodeGlobalsPolyfillPlugin({ 12 | process: true, 13 | buffer: true 14 | }) 15 | ] 16 | }) 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /cypress/support/tasks/index.js: -------------------------------------------------------------------------------- 1 | let accessToken 2 | 3 | module.exports = function tasks (on) { 4 | on('task', { 5 | saveToken (token) { 6 | accessToken = token 7 | return accessToken 8 | }, 9 | getToken () { 10 | if (accessToken) { 11 | return accessToken 12 | } 13 | return null 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitlab-cypress", 3 | "version": "1.0.0", 4 | "description": "Sample project to experiment with Cypress for testing the GitLab application", 5 | "scripts": { 6 | "cy:open": "cypress open --config excludeSpecPattern=[cypress/e2e/api/index.cy.js,cypress/e2e/gui/project/projectIssue.cy.js,cypress/e2e/gui/project/projectButIssue.cy.js,cypress/e2e/gui/allButProject.cy.js]", 7 | "lint": "standard --verbose | snazzy", 8 | "lint:fix": "standard --fix", 9 | "test": "cypress run --spec 'cypress/e2e/gui/profile/createAccessToken.cy.js,cypress/e2e/api/**/*.cy.js,cypress/e2e/gui/**/*.cy.js' --config excludeSpecPattern=[cypress/e2e/api/index.cy.js,cypress/e2e/gui/project/projectIssue.cy.js,cypress/e2e/gui/project/projectButIssue.cy.js,cypress/e2e/gui/allButProject.cy.js,cypress/e2e/gui/profile/deleteAccessTokens.cy.js]", 10 | "test:api:cloud": "cypress run --record --tag 'api' --spec 'cypress/e2e/api/index.cy.js'", 11 | "test:gui:project:issue:cloud": "cypress run --record --tag 'gui:project:issue' --spec 'cypress/e2e/gui/project/projectIssue.cy.js'", 12 | "test:gui:project:but:issue:cloud": "cypress run --record --tag 'gui:project:but:issue' --spec 'cypress/e2e/gui/project/projectButIssue.cy.js'", 13 | "test:gui:all:but:project:cloud": "cypress run --record --tag 'gui:all:but:project' --spec 'cypress/e2e/gui/allButProject.cy.js'", 14 | "test:api": "cypress run --spec 'cypress/e2e/api/index.cy.js'", 15 | "test:gui:project:issue": "cypress run --spec 'cypress/e2e/gui/project/projectIssue.cy.js'", 16 | "test:gui:project:but:issue": "cypress run --spec 'cypress/e2e/gui/project/projectButIssue.cy.js'", 17 | "test:gui:all:but:project": "cypress run --spec 'cypress/e2e/gui/allButProject.cy.js'" 18 | }, 19 | "standard": { 20 | "globals": [ 21 | "before", 22 | "beforeEach", 23 | "cy", 24 | "Cypress", 25 | "describe", 26 | "expect", 27 | "it" 28 | ] 29 | }, 30 | "keywords": [ 31 | "testing", 32 | "automation", 33 | "cypress", 34 | "gitlab", 35 | "web-testing" 36 | ], 37 | "repository": { 38 | "type": "git", 39 | "url": "https://gitlab.com/wlsf82/gitlab-cypress" 40 | }, 41 | "author": "Walmyr Filho ", 42 | "license": "MIT", 43 | "devDependencies": { 44 | "@bahmutov/cypress-esbuild-preprocessor": "^2.2.5", 45 | "@esbuild-plugins/node-globals-polyfill": "^0.2.3", 46 | "@esbuild-plugins/node-modules-polyfill": "^0.2.2", 47 | "@faker-js/faker": "^9.8.0", 48 | "cypress": "^14.4.1", 49 | "esbuild": "^0.25.5", 50 | "eslint": "^8.57.1", 51 | "snazzy": "^9.0.0", 52 | "standard": "^17.1.2" 53 | } 54 | } 55 | --------------------------------------------------------------------------------