Skip to content

Commit

Permalink
Add unit, component and e2e tests using vitest and cypress
Browse files Browse the repository at this point in the history
  • Loading branch information
kvestus committed Feb 18, 2024
1 parent 62b43ae commit c9c5e45
Show file tree
Hide file tree
Showing 105 changed files with 7,786 additions and 498 deletions.
4 changes: 4 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Docker compose project name
COMPOSE_PROJECT_NAME=languages_learner

TEST_USER_USERNAME=
TEST_USER_PASSWORD=
1 change: 1 addition & 0 deletions .idea/template-vite-vue3.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions cypress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig } from 'cypress'
import dotenv from 'dotenv'

dotenv.config({ path: '.env' })
dotenv.config()

export default defineConfig({
chromeWebSecurity: false,
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
},
},
env: {
language: 'xx',
testUser: {
username: process.env.TEST_USER_USERNAME,
password: process.env.TEST_USER_PASSWORD,
},
},
e2e: {
baseUrl: 'http://localhost:3000',
},
})
9 changes: 9 additions & 0 deletions cypress.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type mount } from 'cypress/vue'

declare global {
namespace Cypress {
interface Chainable {
mount: typeof mount;
}
}
}
17 changes: 17 additions & 0 deletions cypress/e2e.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/// <reference types="cypress" />

declare namespace Cypress {
interface LoginOptions {
username?: string
password?: string
type?: 'signin' | 'signup'
visitLoginPage?: boolean
validateAuth?: boolean
}
interface Chainable {
auth(options: LoginOptions = {}): Chainable<never>
authWithoutSession(options: LoginOptions = {}): Chainable<never>
logout(): Chainable<never>
waitWorkspacePageInit(): Chainable<never>
}
}
108 changes: 108 additions & 0 deletions cypress/e2e/auth.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/// <reference types="cypress" />

import { elSelector, withLang } from '@@/cypress/utils'
import { EDataTest, EDataTestClass } from '@/enums/EDataTest'

describe('user sign-in, sign-up and logout', () => {
beforeEach(() => {
cy.visit(withLang())
cy.logout()
})

it('should redirect unauthenticated user to landing page', () => {
cy.visit(withLang('/dictionary'))
cy.location('pathname').should('equal', `/${Cypress.env('language')}`)
})

it('should allow to sign-in and logout', () => {
cy
.authWithoutSession()
.elByClass(EDataTestClass.app_notifications).should('be.visible').and('contain', 'successful_authorization')
.location('pathname').should('equal', withLang('/dictionary'))

.el(EDataTest.workspace_header_user_avatar).trigger('mouseenter')
.get('.n-dropdown-option').contains('sign_out').click()
.location('pathname').should('equal', withLang())
})

it('should display sign-in errors', () => {
const authorizationError = 'authorization_error'
const errorMessageInvalidEmail = 'Firebase: Error (auth/invalid-email)'
const errorMessageNotFound = 'Firebase: Error (auth/user-not-found)'
const errorMessageMissingPassword = 'Firebase: Error (auth/missing-password)'

cy.authWithoutSession({
username: 'not_email',
password: '1',
type: 'signin',
visitLoginPage: true,
validateAuth: false,
})
cy.elByClass(EDataTestClass.app_notifications)
.should('be.visible')
.and('contain', authorizationError)
.and('contain', errorMessageInvalidEmail)
cy
.el(EDataTest.authentication_modal_error).should('be.visible').contains(errorMessageInvalidEmail)
.get(`${elSelector(EDataTest.authentication_modal)} .n-base-close`).click()

cy.authWithoutSession({
username: '',
password: '',
type: 'signin',
visitLoginPage: false,
validateAuth: false,
})
cy.elByClass(EDataTestClass.app_notifications)
.should('be.visible')
.and('contain', authorizationError)
.and('contain', errorMessageInvalidEmail)
cy
.el(EDataTest.authentication_modal_error).should('be.visible').contains(errorMessageInvalidEmail)
.get(`${elSelector(EDataTest.authentication_modal)} .n-base-close`).click()

cy.authWithoutSession({
username: '[email protected]',
password: '1',
type: 'signin',
visitLoginPage: false,
validateAuth: false,
})
cy.elByClass(EDataTestClass.app_notifications)
.should('be.visible')
.and('contain', authorizationError)
.and('contain', errorMessageNotFound)
cy
.el(EDataTest.authentication_modal_error).should('be.visible').contains(errorMessageNotFound)
.get(`${elSelector(EDataTest.authentication_modal)} .n-base-close`).click()

cy.authWithoutSession({
username: '[email protected]',
password: '',
type: 'signin',
visitLoginPage: false,
validateAuth: false,
})
cy.elByClass(EDataTestClass.app_notifications)
.should('be.visible')
.and('contain', authorizationError)
.and('contain', errorMessageMissingPassword)
cy
.el(EDataTest.authentication_modal_error).should('be.visible').contains(errorMessageMissingPassword)
.get(`${elSelector(EDataTest.authentication_modal)} .n-base-close`).click()
})

it('should display sign-up error for existing use', () => {
cy.authWithoutSession({
...(Cypress.env('testUser') ?? {}),
type: 'signup',
validateAuth: false,
})
const errorMessage = 'Firebase: Error (auth/email-already-in-use)'
cy.elByClass(EDataTestClass.app_notifications)
.should('be.visible')
.and('contain', errorMessage)
.and('contain', 'registration_error')
cy.el(EDataTest.authentication_modal_error).should('be.visible').contains(errorMessage)
})
})
218 changes: 218 additions & 0 deletions cypress/e2e/dictionary.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
/// <reference types="cypress" />

import { elSelector, withLang } from '@@/cypress/utils'
import { EWordStatus } from '@/services/dbstore/dto/Words'
import { EDataTest, EDataTestClass } from '@/enums/EDataTest'

describe('workspace dictionary', () => {
beforeEach(() => {
cy.auth()
})

describe('reachability', () => {
it('navigate to /dictionary through the menu and view the list of words with clear filters', () => {
cy
.log('visit dictionary using landing button')
.visit(withLang())
.el(EDataTest.landing_go_to_workspace_button).click()
.location('pathname').should('equal', withLang('/dictionary'))

.log('visit dictionary using workspace menu')
.visit(withLang('/trainings'))
.el(EDataTest.workspace_navigation_item).contains('dictionary').click()
.location('pathname').should('equal', withLang('/dictionary'))

.log('check if filters are clear')
.el(EDataTest.words_container_header_checkbox).should('not.be.checked')
.el(EDataTest.words_container_header_search).should('have.value', '')
.elByClass(EDataTestClass.words_container_header_status_active).contains('all')

// For mobile
// cy.el(EDataTest.words_container_header_status)
// .contains('All')
})
})

describe('functionality', () => {
beforeEach(() => {
cy.visit(withLang('/dictionary'))
cy.waitWorkspacePageInit()
cy.el(EDataTest.words_list_loader).should('not.exist')
})

it('add word, edit word translations, status, and delete word', () => {
const wordSource = `word-${new Date().valueOf()}`
const translations = ['translation1', 'translation2', 'translation3']

cy
.log('create new word')
.get(`${elSelector(EDataTest.words_container_header_search)} input`).type(wordSource)
.el(EDataTest.words_list_loader).should('not.exist')
.el(EDataTest.words_list_item).should('not.exist')
.el(EDataTest.words_creator).should('contain.text', 'no_suitable_words')
.el(EDataTest.words_container_header_add_word_button).click()
.el(EDataTest.words_creator).should('contain.text', 'add_new_word')

.log('set three translations')
.get(`${elSelector(EDataTest.words_creator_translations)} button`).type(translations[0]).clickOutside()
.el(EDataTest.words_creator_add_button).should('exist')
.get(`${elSelector(EDataTest.words_creator_translations)} button`).eq(1).type(translations[1]).clickOutside()
.get(`${elSelector(EDataTest.words_creator_translations)} button`).eq(2).type(translations[2]).clickOutside()

.log('delete second translation')
.get(`${elSelector(EDataTest.words_creator_translations)} button`).eq(1).click()

.log('invoke creation')
.el(EDataTest.words_creator_add_button).click()

.log('check if word added with correct translations')
.el(EDataTest.words_creator).should('not.exist')
.el(EDataTest.words_container_header_add_word_button).should('not.exist')
.el(EDataTest.words_list_loader).should('not.exist')
.get(`${elSelector(EDataTest.words_container_header_search)} input`).should('have.value', wordSource)
.get(`${elSelector(EDataTest.words_list_item)} ${elSelector(EDataTest.words_list_item_source_word)}`).first().should('contain.text', wordSource)
.get(`${elSelector(EDataTest.words_list_item)} ${elSelector(EDataTest.words_list_item_translations)}`).first()
.should('contain.text', translations[0])
.and('not.contain.text', translations[1])
.and('contain.text', translations[2])

const wordSourceEdited = wordSource.slice(0, wordSource.length - 1)
cy
.log('create similar word')
.get(`${elSelector(EDataTest.words_container_header_search)} input`).clear().type(wordSourceEdited)
.el(EDataTest.words_list_loader).should('not.exist')
.el(EDataTest.words_list_item).should('exist')
.el(EDataTest.words_creator).should('not.exist')
.el(EDataTest.words_container_header_add_word_button).click()
.el(EDataTest.words_creator).should('exist').and('contain.text', 'add_new_word')

.log('set translation')
.get(`${elSelector(EDataTest.words_creator_translations)} button`).type(translations[0]).clickOutside()

.log('invoke creation')
.el(EDataTest.words_creator_add_button).click()

.log('check if two words with similar translations found')
.el(EDataTest.words_list_item).should('have.length', 2)

cy
.get(`${elSelector(EDataTest.words_container_header_search)} input`).clear()
.el(EDataTest.words_list_loader).should('not.exist')

cy
.log('change word status')
.el(EDataTest.words_list_item_status).eq(0).trigger('mouseenter')
.get('.n-base-select-menu-option-wrapper > :nth-child(2)').click()

.log('check if status changed')
.el(EDataTest.words_list_item_status).eq(0).should('have.attr', 'data-test-value', EWordStatus.LEARN)

const newTranslation = `translation-${new Date().valueOf()}`
cy
.log('add translation to existing word')
.el(EDataTest.words_list_item_edit_button).eq(0).click()
.get(`${elSelector(EDataTest.words_list_item_edit_translations)} button`).last().type(newTranslation).clickOutside()
.el(EDataTest.words_list_item_edit_translations).should('contain.text', newTranslation)

.log('delete word by deleting all translations')
.get(`${elSelector(EDataTest.words_list_item_edit_translations)} button`).eq(-2).click()
.el(EDataTest.words_list_item_edit_translations).should('not.contain.text', newTranslation)
.get(`${elSelector(EDataTest.words_list_item_edit_translations)} button`).each((_, index, $btns) => {
if (index !== $btns.length - 1) {
// Click to the first element each time
cy.get(`${elSelector(EDataTest.words_list_item_edit_translations)} button`).eq(0).then(($btn) => {
cy.wrap($btn).click()
})
cy.wait(1000)
}
})
.el(EDataTest.words_list_item_edit_translations).should('not.exist')

.log('check if word deleted')
.get(`${elSelector(EDataTest.words_list_item)} ${elSelector(EDataTest.words_list_item_source_word)}`).first().should('not.have.text', wordSourceEdited)

cy
.log('delete word using delete button')
.el(EDataTest.words_list_item_delete_button).eq(0).click()

.log('check if word deleted')
.get(`${elSelector(EDataTest.words_list_item)} ${elSelector(EDataTest.words_list_item_source_word)}`).first().should('not.contain.text', wordSource)
})

// Note: test account has words
it('select words', () => {
cy
.log('select all words using header checkbox')
.el(EDataTest.words_container_header_checkbox).should('not.have.class', 'n-checkbox--checked')
.click().should('have.class', 'n-checkbox--checked')

.log('check if all words selected')
.el(EDataTest.words_list_item_checkbox).should('have.class', 'n-checkbox--checked')

.log('unselect first word')
.el(EDataTest.words_list_item_checkbox).eq(0).click()

.log('check if not all items selected')
.el(EDataTest.words_container_header_checkbox).should('not.have.class', 'n-checkbox--checked')

.log('select all words using header checkbox')
.el(EDataTest.words_container_header_checkbox).click()

.log('check if all words selected')
.el(EDataTest.words_list_item_checkbox).should('have.class', 'n-checkbox--checked')

.log('unselect all words using header checkbox')
.el(EDataTest.words_container_header_checkbox).click()

.log('check if all words unselected')
.el(EDataTest.words_list_item_checkbox).should('not.have.class', 'n-checkbox--checked')
})

// Note: test account has words with the necessary source words and statuses
it('filter by text and status', () => {
const searchText = 'test1'
cy
.log('enter search text with all words selected')
.el(EDataTest.words_container_header_checkbox).click()
.get(`${elSelector(EDataTest.words_container_header_search)} input`).type(searchText).should('have.value', searchText)

.log('check if all words unselected')
.el(EDataTest.words_list_item_checkbox).should('not.have.class', 'n-checkbox--checked')

.log('check if all source words contain search word')
.el(EDataTest.words_list_item_source_word).should('contain', searchText)

.log('change filtered status to "Learned"')
.el(EDataTest.words_container_header_status).eq(EWordStatus.LEARNED + 1).click()

.log('check if words not found')
.el(EDataTest.words_list_item).should('not.exist')

.log('change filtered status to "Learn"')
.el(EDataTest.words_container_header_status).eq(EWordStatus.NEW_WORD + 1).click()

.log('check if words found')
.el(EDataTest.words_list_item).should('exist')

.log('check if each founded word has filtered status')
.el(EDataTest.words_list_item_status).should('have.attr', 'data-test-value', EWordStatus.NEW_WORD)

.log('change status of first word to "Learn"')
.el(EDataTest.words_list_item_status).eq(0).trigger('mouseenter')
.get('.n-base-select-menu-option-wrapper > :nth-child(2)').click()

.log('check if word with another status still in list')
.el(EDataTest.words_list_item_status).eq(0).should('have.attr', 'data-test-value', EWordStatus.LEARN)

.log('recover word status')
.el(EDataTest.words_list_item_status).eq(0).trigger('mouseenter')
.get('.n-base-select-menu-option-wrapper > :nth-child(1)').click()

.log('change filtered text')
.get(`${elSelector(EDataTest.words_container_header_search)} input`).clear().should('have.value', '')

.log('check if filtered status changed to "All"')
.elByClass(EDataTestClass.words_container_header_status_active).contains('all')
})
})
})
Loading

0 comments on commit c9c5e45

Please sign in to comment.