Merge branch 'master' into 2685-Desktop-Sub-Menu

This commit is contained in:
Hannes Heine 2023-03-03 11:00:53 +01:00 committed by GitHub
commit b42801c1ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 232 additions and 123 deletions

View File

@ -550,7 +550,7 @@ jobs:
run: | run: |
cd e2e-tests/ cd e2e-tests/
yarn yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature,cypress/e2e/User.Registration.feature
- name: End-to-end tests | if tests failed, upload screenshots - name: End-to-end tests | if tests failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }} if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3

View File

@ -1,13 +1,13 @@
import gql from 'graphql-tag' import gql from 'graphql-tag'
export const listAllContributions = gql` export const adminListAllContributions = gql`
query ( query (
$currentPage: Int = 1 $currentPage: Int = 1
$pageSize: Int = 25 $pageSize: Int = 25
$order: Order = DESC $order: Order = DESC
$statusFilter: [ContributionStatus!] $statusFilter: [ContributionStatus!]
) { ) {
listAllContributions( adminListAllContributions(
currentPage: $currentPage currentPage: $currentPage
pageSize: $pageSize pageSize: $pageSize
order: $order order: $order
@ -28,6 +28,8 @@ export const listAllContributions = gql`
messagesCount messagesCount
deniedAt deniedAt
deniedBy deniedBy
deletedAt
deletedBy
} }
} }
} }

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm' import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { listAllContributions } from '../graphql/listAllContributions' import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
@ -38,7 +38,7 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
listAllContributions: { adminListAllContributions: {
contributionCount: 2, contributionCount: 2,
contributionList: [ contributionList: [
{ {
@ -92,14 +92,14 @@ const defaultData = () => {
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
let wrapper let wrapper
const adminListAllContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn() const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn() const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn() const confirmContributionMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
listAllContributions, adminListAllContributions,
jest adminListAllContributionsMock
.fn()
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -331,78 +331,82 @@ describe('CreationConfirm', () => {
describe('filter tabs', () => { describe('filter tabs', () => {
describe('click tab "confirmed"', () => { describe('click tab "confirmed"', () => {
let refetchSpy
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="confirmed"]').trigger('click') await wrapper.find('a[data-test="confirmed"]').trigger('click')
}) })
it('has statusFilter set to ["CONFIRMED"]', () => { it('refetches contributions with proper filter', () => {
expect( expect(adminListAllContributionsMock).toBeCalledWith({
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables, currentPage: 1,
).toMatchObject({ statusFilter: ['CONFIRMED'] }) order: 'DESC',
}) pageSize: 25,
statusFilter: ['CONFIRMED'],
it('refetches contributions', () => { })
expect(refetchSpy).toBeCalled()
}) })
describe('click tab "open"', () => { describe('click tab "open"', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="open"]').trigger('click') await wrapper.find('a[data-test="open"]').trigger('click')
}) })
it('has statusFilter set to ["IN_PROGRESS", "PENDING"]', () => { it('refetches contributions with proper filter', () => {
expect( expect(adminListAllContributionsMock).toBeCalledWith({
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables, currentPage: 1,
).toMatchObject({ statusFilter: ['IN_PROGRESS', 'PENDING'] }) order: 'DESC',
}) pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
it('refetches contributions', () => { })
expect(refetchSpy).toBeCalled()
}) })
}) })
describe('click tab "denied"', () => { describe('click tab "denied"', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="denied"]').trigger('click') await wrapper.find('a[data-test="denied"]').trigger('click')
}) })
it('has statusFilter set to ["DENIED"]', () => { it('refetches contributions with proper filter', () => {
expect( expect(adminListAllContributionsMock).toBeCalledWith({
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables, currentPage: 1,
).toMatchObject({ statusFilter: ['DENIED'] }) order: 'DESC',
pageSize: 25,
statusFilter: ['DENIED'],
})
})
})
describe('click tab "deleted"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="deleted"]').trigger('click')
}) })
it('refetches contributions', () => { it('refetches contributions with proper filter', () => {
expect(refetchSpy).toBeCalled() expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['DELETED'],
})
}) })
}) })
describe('click tab "all"', () => { describe('click tab "all"', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="all"]').trigger('click') await wrapper.find('a[data-test="all"]').trigger('click')
}) })
it('has statusFilter set to ["IN_PROGRESS", "PENDING", "CONFIRMED", "DENIED", "DELETED"]', () => { it('refetches contributions with proper filter', () => {
expect( expect(adminListAllContributionsMock).toBeCalledWith({
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables, currentPage: 1,
).toMatchObject({ order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'], statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
}) })
}) })
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
}) })
}) })
}) })
@ -412,10 +416,20 @@ describe('CreationConfirm', () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2) await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
}) })
it.skip('updates the status', () => { it('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1) expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS') expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
}) })
}) })
describe('unknown variant', () => {
beforeEach(async () => {
await wrapper.setData({ variant: 'unknown' })
})
it('has overlay icon "info"', () => {
expect(wrapper.vm.overlayIcon).toBe('info')
})
})
}) })
}) })

View File

@ -73,7 +73,7 @@
<script> <script>
import Overlay from '../components/Overlay' import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable' import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { listAllContributions } from '../graphql/listAllContributions' import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
@ -173,15 +173,11 @@ export default {
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS' this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
}, },
}, },
watch: {
statusFilter() {
this.$apollo.queries.ListAllContributions.refetch()
},
},
computed: { computed: {
fields() { fields() {
return [ return [
[ [
// open contributions
{ key: 'bookmark', label: this.$t('delete') }, { key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') }, { key: 'deny', label: this.$t('deny') },
{ key: 'email', label: this.$t('e_mail') }, { key: 'email', label: this.$t('e_mail') },
@ -207,6 +203,7 @@ export default {
{ key: 'confirm', label: this.$t('save') }, { key: 'confirm', label: this.$t('save') },
], ],
[ [
// confirmed contributions
{ key: 'firstName', label: this.$t('firstname') }, { key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
{ {
@ -241,6 +238,7 @@ export default {
{ key: 'chatCreation', label: this.$t('chat') }, { key: 'chatCreation', label: this.$t('chat') },
], ],
[ [
// denied contributions
{ key: 'reActive', label: 'reActive' }, { key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') }, { key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
@ -276,8 +274,45 @@ export default {
{ key: 'deniedBy', label: this.$t('mod') }, { key: 'deniedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') }, { key: 'chatCreation', label: this.$t('chat') },
], ],
[],
[ [
// deleted contributions
{ key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{
key: 'deletedAt',
label: this.$t('contributions.deleted'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
},
},
{ key: 'deletedBy', label: this.$t('mod') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: 'state' }, { key: 'state', label: 'state' },
{ key: 'firstName', label: this.$t('firstname') }, { key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') }, { key: 'lastName', label: this.$t('lastname') },
@ -349,19 +384,19 @@ export default {
apollo: { apollo: {
ListAllContributions: { ListAllContributions: {
query() { query() {
return listAllContributions return adminListAllContributions
}, },
variables() { variables() {
// may be at some point we need a pagination here
return { return {
currentPage: this.currentPage, currentPage: this.currentPage,
pageSize: this.pageSize, pageSize: this.pageSize,
statusFilter: this.statusFilter, statusFilter: this.statusFilter,
} }
}, },
update({ listAllContributions }) { fetchPolicy: 'no-cache',
this.rows = listAllContributions.contributionCount update({ adminListAllContributions }) {
this.items = listAllContributions.contributionList this.rows = adminListAllContributions.contributionCount
this.items = adminListAllContributions.contributionList
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Overview from './Overview' import Overview from './Overview'
import { listAllContributions } from '../graphql/listAllContributions' import { adminListAllContributions } from '../graphql/adminListAllContributions'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client' import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup' import { toastErrorSpy } from '../../test/testSetup'
@ -30,7 +30,7 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
listAllContributions: { adminListAllContributions: {
contributionCount: 2, contributionCount: 2,
contributionList: [ contributionList: [
{ {
@ -84,11 +84,11 @@ const defaultData = () => {
describe('Overview', () => { describe('Overview', () => {
let wrapper let wrapper
const listAllContributionsMock = jest.fn() const adminListAllContributionsMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
listAllContributions, adminListAllContributions,
listAllContributionsMock adminListAllContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -109,8 +109,8 @@ describe('Overview', () => {
}) })
}) })
it('calls the listAllContributions query', () => { it('calls the adminListAllContributions query', () => {
expect(listAllContributionsMock).toBeCalledWith({ expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1, currentPage: 1,
order: 'DESC', order: 'DESC',
pageSize: 25, pageSize: 25,

View File

@ -31,7 +31,7 @@
</div> </div>
</template> </template>
<script> <script>
import { listAllContributions } from '../graphql/listAllContributions' import { adminListAllContributions } from '../graphql/adminListAllContributions'
export default { export default {
name: 'overview', name: 'overview',
@ -43,7 +43,7 @@ export default {
apollo: { apollo: {
AllContributions: { AllContributions: {
query() { query() {
return listAllContributions return adminListAllContributions
}, },
variables() { variables() {
// may be at some point we need a pagination here // may be at some point we need a pagination here
@ -51,8 +51,8 @@ export default {
statusFilter: this.statusFilter, statusFilter: this.statusFilter,
} }
}, },
update({ listAllContributions }) { update({ adminListAllContributions }) {
this.$store.commit('setOpenCreations', listAllContributions.contributionCount) this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
}, },
error({ message }) { error({ message }) {
this.toastError(message) this.toastError(message)

View File

@ -8,6 +8,7 @@ CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234' CONFIG.EMAIL_SMTP_PORT = '1234'
CONFIG.EMAIL_USERNAME = 'user' CONFIG.EMAIL_USERNAME = 'user'
CONFIG.EMAIL_PASSWORD = 'pwd' CONFIG.EMAIL_PASSWORD = 'pwd'
CONFIG.EMAIL_TLS = true
jest.mock('nodemailer', () => { jest.mock('nodemailer', () => {
return { return {

View File

@ -12,7 +12,6 @@ export class Contribution {
this.amount = contribution.amount this.amount = contribution.amount
this.memo = contribution.memo this.memo = contribution.memo
this.createdAt = contribution.createdAt this.createdAt = contribution.createdAt
this.deletedAt = contribution.deletedAt
this.confirmedAt = contribution.confirmedAt this.confirmedAt = contribution.confirmedAt
this.confirmedBy = contribution.confirmedBy this.confirmedBy = contribution.confirmedBy
this.contributionDate = contribution.contributionDate this.contributionDate = contribution.contributionDate
@ -20,6 +19,8 @@ export class Contribution {
this.messagesCount = contribution.messages ? contribution.messages.length : 0 this.messagesCount = contribution.messages ? contribution.messages.length : 0
this.deniedAt = contribution.deniedAt this.deniedAt = contribution.deniedAt
this.deniedBy = contribution.deniedBy this.deniedBy = contribution.deniedBy
this.deletedAt = contribution.deletedAt
this.deletedBy = contribution.deletedBy
} }
@Field(() => Number) @Field(() => Number)
@ -40,9 +41,6 @@ export class Contribution {
@Field(() => Date) @Field(() => Date)
createdAt: Date createdAt: Date
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Date, { nullable: true }) @Field(() => Date, { nullable: true })
confirmedAt: Date | null confirmedAt: Date | null
@ -55,6 +53,12 @@ export class Contribution {
@Field(() => Number, { nullable: true }) @Field(() => Number, { nullable: true })
deniedBy: number | null deniedBy: number | null
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Number, { nullable: true })
deletedBy: number | null
@Field(() => Date) @Field(() => Date)
contributionDate: Date contributionDate: Date

View File

@ -24,7 +24,11 @@ import {
listContributions, listContributions,
adminListAllContributions, adminListAllContributions,
} from '@/seeds/graphql/queries' } from '@/seeds/graphql/queries'
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants' import {
sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants'
import { import {
cleanDB, cleanDB,
resetToken, resetToken,
@ -50,21 +54,7 @@ import { ContributionListResult } from '@model/Contribution'
import { ContributionStatus } from '@enum/ContributionStatus' import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order' import { Order } from '@enum/Order'
// mock account activation email to avoid console spam jest.mock('@/emails/sendEmailVariants')
jest.mock('@/emails/sendEmailVariants', () => {
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
return {
__esModule: true,
...originalModule,
// TODO: test the call of …
// sendAccountActivationEmail: jest.fn((a) => originalModule.sendAccountActivationEmail(a)),
sendContributionConfirmedEmail: jest.fn((a) =>
originalModule.sendContributionConfirmedEmail(a),
),
// TODO: test the call of …
// sendContributionRejectedEmail: jest.fn((a) => originalModule.sendContributionRejectedEmail(a)),
}
})
let mutate: any, query: any, con: any let mutate: any, query: any, con: any
let testEnv: any let testEnv: any
@ -829,6 +819,18 @@ describe('ContributionResolver', () => {
}), }),
) )
}) })
it('calls sendContributionDeniedEmail', async () => {
expect(sendContributionDeniedEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
senderFirstName: 'Peter',
senderLastName: 'Lustig',
contributionMemo: 'Test contribution to deny',
})
})
}) })
}) })
}) })
@ -2384,6 +2386,18 @@ describe('ContributionResolver', () => {
}), }),
) )
}) })
it('calls sendContributionDeletedEmail', async () => {
expect(sendContributionDeletedEmail).toBeCalledWith({
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
language: 'de',
senderFirstName: 'Peter',
senderLastName: 'Lustig',
contributionMemo: 'Das war leider zu Viel!',
})
})
}) })
describe('creation already confirmed', () => { describe('creation already confirmed', () => {

View File

@ -2,7 +2,7 @@ import { defineConfig } from 'cypress'
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor' import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
import browserify from '@badeball/cypress-cucumber-preprocessor/browserify' import browserify from '@badeball/cypress-cucumber-preprocessor/browserify'
let resetPasswordLink: string let emailLink: string
async function setupNodeEvents( async function setupNodeEvents(
on: Cypress.PluginEvents, on: Cypress.PluginEvents,
@ -18,11 +18,11 @@ async function setupNodeEvents(
) )
on('task', { on('task', {
setResetPasswordLink: (val) => { setEmailLink: (link: string) => {
return (resetPasswordLink = val) return (emailLink = link)
}, },
getResetPasswordLink: () => { getEmailLink: () => {
return resetPasswordLink return emailLink
}, },
}) })

View File

@ -13,8 +13,8 @@ Feature: User Authentication - reset password
And the user navigates to the forgot password page And the user navigates to the forgot password page
When the user enters the e-mail address "bibi@bloxberg.de" When the user enters the e-mail address "bibi@bloxberg.de"
And the user submits the e-mail form And the user submits the e-mail form
Then the user receives an e-mail containing the password reset link Then the user receives an e-mail containing the "password reset" link
When the user opens the password reset link in the browser When the user opens the "password reset" link in the browser
And the user enters the password "12345Aa_" And the user enters the password "12345Aa_"
And the user repeats the password "12345Aa_" And the user repeats the password "12345Aa_"
And the user submits the password form And the user submits the password form

View File

@ -2,12 +2,16 @@ Feature: User registration
As a user As a user
I want to register to create an account I want to register to create an account
@skip
Scenario: Register successfully Scenario: Register successfully
Given the user navigates to page "/register" Given the user navigates to page "/register"
When the user fills name and email "Regina" "Register" "regina@register.com" When the user fills name and email "Regina" "Register" "regina@register.com"
And the user agrees to the privacy policy And the user agrees to the privacy policy
And the user submits the registration form And the user submits the registration form
Then the user can use a provided activation link Then the user receives an e-mail containing the "activation" link
And the user can set a password "Aa12345_" When the user opens the "activation" link in the browser
And the user can login with the credentials "regina@register.com" "Aa12345_" And the user enters the password "12345Aa_"
And the user repeats the password "12345Aa_"
And the user submits the password form
And the user clicks the sign in button
Then the user submits the credentials "regina@register.com" "12345Aa_"
And the user is logged in with username "Regina Register"

View File

@ -4,7 +4,7 @@ export class RegistrationPage {
// selectors // selectors
firstnameInput = '#registerFirstname' firstnameInput = '#registerFirstname'
lastnameInput = '#registerLastname' lastnameInput = '#registerLastname'
emailInput = '#Email-input-field' emailInput = 'input[type=email]'
checkbox = '#registerCheckbox' checkbox = '#registerCheckbox'
submitBtn = '[type=submit]' submitBtn = '[type=submit]'
@ -35,7 +35,7 @@ export class RegistrationPage {
cy.get(this.checkbox).click({ force: true }) cy.get(this.checkbox).click({ force: true })
} }
submitRegistrationPage() { submitRegistrationForm() {
cy.get(this.submitBtn).should('be.enabled') cy.get(this.submitBtn).should('be.enabled')
cy.get(this.submitBtn).click() cy.get(this.submitBtn).click()
} }

View File

@ -2,19 +2,19 @@
export class ResetPasswordPage { export class ResetPasswordPage {
// selectors // selectors
newPasswordBlock = '#new-password-input-field' newPasswordInput = '#new-password-input-field'
newPasswordRepeatBlock = '#repeat-new-password-input-field' newPasswordRepeatInput = '#repeat-new-password-input-field'
resetPasswordBtn = 'button[type=submit]' resetPasswordBtn = 'button[type=submit]'
resetPasswordMessageBlock = '[data-test="reset-password-message"]' resetPasswordMessageBlock = '[data-test="reset-password-message"]'
signinBtn = '.btn.test-message-button' signinBtn = '.btn.test-message-button'
enterNewPassword(password: string) { enterNewPassword(password: string) {
cy.get(this.newPasswordBlock).find('input[type=password]').type(password) cy.get(this.newPasswordInput).find('input[type=password]').type(password)
return this return this
} }
repeatNewPassword(password: string) { repeatNewPassword(password: string) {
cy.get(this.newPasswordRepeatBlock) cy.get(this.newPasswordRepeatInput)
.find('input[type=password]') .find('input[type=password]')
.type(password) .type(password)
return this return this

View File

@ -5,41 +5,55 @@ import { UserEMailSite } from '../../e2e/models/UserEMailSite'
const userEMailSite = new UserEMailSite() const userEMailSite = new UserEMailSite()
const resetPasswordPage = new ResetPasswordPage() const resetPasswordPage = new ResetPasswordPage()
Then('the user receives an e-mail containing the password reset link', () => { Then('the user receives an e-mail containing the {string} link', (linkName: string) => {
let emailSubject: string
let linkPattern: RegExp
switch (linkName) {
case 'activation':
emailSubject = 'Email Verification'
linkPattern = /\/checkEmail\/[0-9]+\d/
break
case 'password reset':
emailSubject = 'asswor'
linkPattern = /\/reset-password\/[0-9]+\d/
break
default:
throw new Error(`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect linkname string "${linkName}"`)
}
cy.origin( cy.origin(
Cypress.env('mailserverURL'), Cypress.env('mailserverURL'),
{ args: userEMailSite }, { args: { emailSubject, linkPattern, userEMailSite } },
(userEMailSite) => { ({ emailSubject, linkPattern, userEMailSite }) => {
const linkPattern = /\/reset-password\/[0-9]+\d/ cy.visit('/') // navigate to user's e-mail site (on fake mail server)
cy.visit('/') // navigate to user's e-maile site (on fake mail server)
cy.get(userEMailSite.emailInbox).should('be.visible') cy.get(userEMailSite.emailInbox).should('be.visible')
cy.get(userEMailSite.emailList) cy.get(userEMailSite.emailList)
.find('.email-item') .find('.email-item')
.filter(':contains(asswor)') .filter(`:contains(${emailSubject})`)
.first() .first()
.click() .click()
cy.get(userEMailSite.emailMeta) cy.get(userEMailSite.emailMeta)
.find(userEMailSite.emailSubject) .find(userEMailSite.emailSubject)
.contains('asswor') .contains(emailSubject)
cy.get('.email-content') cy.get('.email-content', { timeout: 2000})
.find('.plain-text') .find('.plain-text')
.contains(linkPattern) .contains(linkPattern)
.invoke('text') .invoke('text')
.then((text) => { .then((text) => {
const resetPasswordLink = text.match(linkPattern)[0] const emailLink = text.match(linkPattern)[0]
cy.task('setResetPasswordLink', resetPasswordLink) cy.task('setEmailLink', emailLink)
}) })
} }
) )
}) })
When('the user opens the password reset link in the browser', () => { When('the user opens the {string} link in the browser', (linkName: string) => {
cy.task('getResetPasswordLink').then((passwordResetLink) => { cy.task('getEmailLink').then((emailLink) => {
cy.visit(passwordResetLink) cy.visit(emailLink)
}) })
cy.get(resetPasswordPage.newPasswordRepeatBlock).should('be.visible') cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
}) })

View File

@ -18,7 +18,7 @@ And('the user agrees to the privacy policy', () => {
}) })
And('the user submits the registration form', () => { And('the user submits the registration form', () => {
registrationPage.submitRegistrationPage() registrationPage.submitRegistrationForm()
cy.get(registrationPage.RegistrationThanxHeadline).should('be.visible') cy.get(registrationPage.RegistrationThanxHeadline).should('be.visible')
cy.get(registrationPage.RegistrationThanxText).should('be.visible') cy.get(registrationPage.RegistrationThanxText).should('be.visible')
}) })

View File

@ -22,7 +22,7 @@
"@cypress/browserify-preprocessor": "^3.0.2", "@cypress/browserify-preprocessor": "^3.0.2",
"@typescript-eslint/eslint-plugin": "^5.38.0", "@typescript-eslint/eslint-plugin": "^5.38.0",
"@typescript-eslint/parser": "^5.38.0", "@typescript-eslint/parser": "^5.38.0",
"cypress": "^10.4.0", "cypress": "^12.7.0",
"eslint": "^8.23.1", "eslint": "^8.23.1",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",

View File

@ -1,6 +1,23 @@
<template> <template>
<div class="contribution-messages-list-item"> <div class="contribution-messages-list-item">
<div v-if="isNotModerator" class="text-right pr-4 pr-lg-0 is-not-moderator"> <div v-if="message.type === 'HISTORY'">
<b-row class="mb-3 border border-197 p-1">
<b-col cols="10">
<small>{{ $d(new Date(message.createdAt), 'short') }}</small>
<div class="font-weight-bold" data-test="username">
{{ storeName.username }} {{ $t('contribution.isEdited') }}
</div>
<div class="small">
{{ $t('contribution.oldContribution') }}
</div>
<parse-message v-bind="message" data-test="message" class="p-2"></parse-message>
</b-col>
<b-col cols="2">
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
</b-col>
</b-row>
</div>
<div v-else-if="isNotModerator" class="text-right pr-4 pr-lg-0 is-not-moderator">
<b-row class="mb-3"> <b-row class="mb-3">
<b-col cols="10"> <b-col cols="10">
<div class="font-weight-bold" data-test="username">{{ storeName.username }}</div> <div class="font-weight-bold" data-test="username">{{ storeName.username }}</div>

View File

@ -56,6 +56,7 @@
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.", "openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
"yourContribution": "Dein Beitrag zum Gemeinwohl" "yourContribution": "Dein Beitrag zum Gemeinwohl"
}, },
"isEdited": "hat den Beitrag bearbeitet",
"lastContribution": "Letzte Beiträge", "lastContribution": "Letzte Beiträge",
"noContributions": { "noContributions": {
"allContributions": "Es wurden noch keine Beiträge eingereicht.", "allContributions": "Es wurden noch keine Beiträge eingereicht.",
@ -67,6 +68,7 @@
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.", "lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
"thisMonth": "Für den aktuellen Monat ist das Schöpfungslimit erreicht." "thisMonth": "Für den aktuellen Monat ist das Schöpfungslimit erreicht."
}, },
"oldContribution": "Vorherige Version",
"selectDate": "Wann war dein Beitrag?", "selectDate": "Wann war dein Beitrag?",
"submit": "Einreichen", "submit": "Einreichen",
"submitted": "Der Beitrag wurde eingereicht.", "submitted": "Der Beitrag wurde eingereicht.",

View File

@ -56,6 +56,7 @@
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.", "openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
"yourContribution": "Your Contributions to the Common Good" "yourContribution": "Your Contributions to the Common Good"
}, },
"isEdited": "edited the contribution",
"lastContribution": "Last Contributions", "lastContribution": "Last Contributions",
"noContributions": { "noContributions": {
"allContributions": "No contributions have been submitted yet.", "allContributions": "No contributions have been submitted yet.",
@ -67,6 +68,7 @@
"lastMonth": "The creation limit is reached for the selected month.", "lastMonth": "The creation limit is reached for the selected month.",
"thisMonth": "The creation limit has been reached for the current month." "thisMonth": "The creation limit has been reached for the current month."
}, },
"oldContribution": "Previous version",
"selectDate": "When was your contribution?", "selectDate": "When was your contribution?",
"submit": "Submit", "submit": "Submit",
"submitted": "The contribution was submitted.", "submitted": "The contribution was submitted.",