fix(frontend): fix postmigration fix (#3382)

* feat(frontend): migration fixes

* feat(admin): post migration fixes

* feat(admin): revert docker change

* feat(admin): update tests

* feat(frontend): add feedback fixes
This commit is contained in:
MateuszMichalowski 2024-11-09 00:11:02 +01:00 committed by GitHub
parent 772ff51581
commit ccc04dd706
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 379 additions and 214 deletions

View File

@ -26,11 +26,10 @@
"@vee-validate/i18n": "^4.13.2",
"@vee-validate/rules": "^4.13.2",
"@vee-validate/yup": "^4.13.2",
"@vitejs/plugin-vue": "3.2.0",
"@vitejs/plugin-vue": "5.1.4",
"@vue-leaflet/vue-leaflet": "^0.10.1",
"@vue/apollo-composable": "^4.0.2",
"@vue/apollo-option": "^4.0.0",
"@vue/compat": "^3.4.31",
"apollo-boost": "^0.4.9",
"autoprefixer": "^10.4.19",
"babel-core": "^7.0.0-bridge.0",
@ -59,13 +58,10 @@
"vite-plugin-commonjs": "^0.10.1",
"vue": "3.4.31",
"vue-apollo": "^3.1.2",
"vue-avatar": "^2.3.3",
"vue-flatpickr-component": "^8.1.2",
"vue-i18n": "^9.13.1",
"vue-loading-overlay": "^3.4.2",
"vue-router": "^4.4.0",
"vue-timer-hook": "^1.0.84",
"vue-timers": "^2.0.4",
"vuex": "^4.1.0",
"vuex-persistedstate": "^4.1.0",
"yup": "^1.4.0"
@ -75,7 +71,6 @@
"@iconify-json/bi": "^1.1.23",
"@intlify/eslint-plugin-vue-i18n": "^1.4.0",
"@vitest/coverage-v8": "^2.0.5",
"@vue/compiler-sfc": "^3.4.35",
"@vue/eslint-config-prettier": "^4.0.1",
"@vue/test-utils": "^2.4.5",
"babel-plugin-component": "^1.1.0",

View File

@ -359,3 +359,12 @@ input.rounded-input {
.fs-7 {
font-size: 14px !important;
}
.vue3-avatar.container {
padding: 0 !important;
}
.avatar {
font-family: inherit !important;
font-weight: normal !important;
}

View File

@ -0,0 +1,135 @@
<template>
<div
class="app-avatar d-flex justify-content-center align-items-center rounded-circle"
:style="{
width: `${size}px`,
height: `${size}px`,
backgroundColor,
textTransform: 'uppercase',
}"
>
<span
:style="{
fontSize: `${size * 0.4}px`,
lineHeight: '1',
color: props.color,
}"
class="font-medium"
>
{{ computedInitials }}
</span>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
size: {
type: Number,
default: 50,
},
color: {
type: String,
default: '',
},
name: {
type: String,
default: '',
},
initials: {
type: String,
default: '',
},
})
// Enhanced color palette with better contrast ratios
const colorPalette = [
{ bg: '#4A5568', text: '#FFFFFF' }, // Slate Blue
{ bg: '#2C7A7B', text: '#FFFFFF' }, // Teal
{ bg: '#805AD5', text: '#FFFFFF' }, // Purple
{ bg: '#DD6B20', text: '#FFFFFF' }, // Orange
{ bg: '#3182CE', text: '#FFFFFF' }, // Blue
{ bg: '#38A169', text: '#FFFFFF' }, // Green
{ bg: '#E53E3E', text: '#FFFFFF' }, // Red
{ bg: '#6B46C1', text: '#FFFFFF' }, // Indigo
{ bg: '#2B6CB0', text: '#FFFFFF' }, // Dark Blue
{ bg: '#9C4221', text: '#FFFFFF' }, // Brown
]
// Generate consistent index based on string
function stringToIndex(str) {
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return Math.abs(hash % colorPalette.length)
}
// Parse any color format to RGB
function parseColor(color) {
const div = document.createElement('div')
div.style.color = color
document.body.appendChild(div)
const computed = window.getComputedStyle(div).color
document.body.removeChild(div)
const match = computed.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/)
if (!match) return [0, 0, 0]
return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]
}
// Calculate relative luminance using WCAG formula
function getLuminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map((c) => {
c = c / 255
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
})
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
}
// Calculate contrast ratio using WCAG formula
function getContrastRatio(l1, l2) {
const lighter = Math.max(l1, l2)
const darker = Math.min(l1, l2)
return (lighter + 0.05) / (darker + 0.05)
}
// Get text color based on background ensuring WCAG AA compliance (4.5:1 minimum)
function getTextColor(backgroundColor) {
const [r, g, b] = parseColor(backgroundColor)
const bgLuminance = getLuminance(r, g, b)
const whiteLuminance = getLuminance(255, 255, 255)
const blackLuminance = getLuminance(0, 0, 0)
const whiteContrast = getContrastRatio(whiteLuminance, bgLuminance)
const blackContrast = getContrastRatio(blackLuminance, bgLuminance)
// Return the color with better contrast (minimum 4.5:1 for WCAG AA)
return whiteContrast >= blackContrast ? '#FFFFFF' : '#000000'
}
const computedInitials = computed(() => {
if (props.initials) return props.initials
return props.name
.split(' ')
.map((word) => word[0])
.join('')
.toUpperCase()
.slice(0, 2)
})
const backgroundColor = computed(() => {
const colorIndex = stringToIndex(computedInitials.value || props.name)
return colorPalette[colorIndex].bg
})
const textColor = computed(() => {
if (props.color) {
return getTextColor(props.color)
}
const colorIndex = stringToIndex(computedInitials.value || props.name)
return colorPalette[colorIndex].text
})
</script>

View File

@ -90,5 +90,4 @@ function setDefaultCommunity() {
}
onMounted(setDefaultCommunity)
onUpdated(setDefaultCommunity)
</script>

View File

@ -16,7 +16,17 @@
<parse-message v-bind="message" data-test="message" class="p-2"></parse-message>
</BCol>
<BCol cols="2">
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
<!-- <avatar-->
<!-- class="vue3-avatar"-->
<!-- :name="storeName.username"-->
<!-- :initials="storeName.initials"-->
<!-- :border="false"-->
<!-- />-->
<app-avatar
class="vue3-avatar"
:name="storeName.username"
:initials="storeName.initials"
/>
</BCol>
</BRow>
</div>
@ -28,14 +38,30 @@
<parse-message v-bind="message" data-test="message"></parse-message>
</BCol>
<BCol cols="2">
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
<!-- <avatar-->
<!-- class="vue3-avatar"-->
<!-- :name="storeName.username"-->
<!-- :initials="storeName.initials"-->
<!-- :border="false"-->
<!-- />-->
<app-avatar
class="vue3-avatar"
:name="storeName.username"
:initials="storeName.initials"
/>
</BCol>
</BRow>
</div>
<div v-else>
<BRow class="mb-3 p-2 is-moderator">
<BCol cols="2">
<avatar :username="moderationName.username" :initials="moderationName.initials"></avatar>
<!-- <avatar-->
<!-- class="vue3-avatar"-->
<!-- :name="moderationName.username"-->
<!-- :initials="moderationName.initials"-->
<!-- :border="false"-->
<!-- />-->
<app-avatar :name="moderationName.username" :initials="moderationName.initials" />
</BCol>
<BCol cols="10">
<div class="font-weight-bold">
@ -54,13 +80,13 @@
</template>
<script>
import Avatar from 'vue-avatar'
import ParseMessage from '@/components/ContributionMessages/ParseMessage'
import AppAvatar from '@/components/AppAvatar.vue'
export default {
name: 'ContributionMessagesListItem',
components: {
Avatar,
AppAvatar,
ParseMessage,
},
props: {

View File

@ -6,13 +6,21 @@
>
<BRow>
<BCol cols="3" lg="2" md="2">
<avatar
<!-- <avatar-->
<!-- v-if="firstName"-->
<!-- :name="username.username"-->
<!-- :initials="username.initials"-->
<!-- :border="false"-->
<!-- color="#fff"-->
<!-- class="vue3-avatar fw-bold"-->
<!-- />-->
<app-avatar
v-if="firstName"
:username="username.username"
:name="username.username"
:initials="username.initials"
color="#fff"
class="fw-bold"
></avatar>
class="vue3-avatar fw-bold"
/>
<BAvatar v-else rounded="lg" :variant="variant" size="4.55em">
<variant-icon :icon="icon" variant="white" />
</BAvatar>
@ -112,13 +120,13 @@
<script setup>
import { ref, computed, watch } from 'vue'
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import ContributionMessagesList from '@/components/ContributionMessages/ContributionMessagesList'
import { listContributionMessages } from '@/graphql/queries'
import { useAppToast } from '@/composables/useToast'
import { useI18n } from 'vue-i18n'
import { useLazyQuery, useQuery } from '@vue/apollo-composable'
import AppAvatar from '@/components/AppAvatar.vue'
const props = defineProps({
id: {

View File

@ -162,10 +162,11 @@ describe('CollapseLinksList', () => {
transactionLinks: [{ id: 1 }, { id: 2 }],
pageSize: 5,
})
await wrapper.vm.$nextTick()
})
it('renders text in plural with page size links to load', () => {
expect(mockT).toHaveBeenCalledWith('link-load', 2, { n: 5 })
it('renders text with pageSize as number of links to load', () => {
expect(mockT).toHaveBeenCalledWith('link-load-more', { n: 5 })
})
})
})

View File

@ -11,27 +11,19 @@
>
<BRow class="mb-4 gap-5">
<BCol>
<BRow class="bg-248 gradido-border-radius position-relative">
<BFormRadio
name="shipping"
size="md"
reverse
:value="SEND_TYPES.send"
class="transaction-form-radio"
>
<BRow
class="bg-248 gradido-border-radius position-relative transaction-form-radio"
>
<BFormRadio name="shipping" size="md" reverse :value="SEND_TYPES.send">
{{ $t('send_gdd') }}
</BFormRadio>
</BRow>
</BCol>
<BCol>
<BRow class="bg-248 gradido-border-radius position-relative">
<BFormRadio
name="shipping"
:value="SEND_TYPES.link"
size="md"
reverse
class="transaction-form-radio"
>
<BRow
class="bg-248 gradido-border-radius position-relative transaction-form-radio"
>
<BFormRadio name="shipping" :value="SEND_TYPES.link" size="md" reverse>
{{ $t('send_per_link') }}
</BFormRadio>
</BRow>
@ -287,7 +279,7 @@ label {
display: flex !important;
justify-content: space-between;
> input {
> div > input {
position: absolute;
right: 35px;
top: 50%;

View File

@ -4,6 +4,7 @@ import { createRouter, createWebHistory, RouterLink } from 'vue-router'
import { createStore } from 'vuex'
import Navbar from './Navbar.vue'
import { BImg, BNavbar, BNavbarBrand, BNavbarNav } from 'bootstrap-vue-next'
import AppAvatar from '@/components/AppAvatar.vue'
// Mock vue-avatar
vi.mock('vue-avatar', () => ({
@ -50,6 +51,7 @@ describe('Navbar', () => {
BNavbarBrand,
BImg,
RouterLink,
AppAvatar,
},
},
props: {
@ -72,11 +74,11 @@ describe('Navbar', () => {
describe('.avatar element', () => {
it('is rendered', () => {
expect(wrapper.findComponent({ name: 'Avatar' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'AppAvatar' }).exists()).toBe(true)
})
it("has the user's initials", () => {
const avatar = wrapper.findComponent({ name: 'Avatar' })
const avatar = wrapper.findComponent({ name: 'AppAvatar' })
expect(avatar.props('initials')).toBe('TU')
})
})

View File

@ -20,12 +20,14 @@
<router-link to="/settings">
<div class="d-flex align-items-center">
<div class="me-3">
<avatar
:username="username.username"
<app-avatar
class="vue3-avatar"
:name="username.username"
:initials="username.initials"
:border="false"
:color="'#fff'"
:size="61"
></avatar>
/>
</div>
<div>
<div data-test="navbar-item-username">{{ username.username }}</div>
@ -45,13 +47,8 @@
</template>
<script>
import Avatar from 'vue-avatar'
export default {
name: 'Navbar',
components: {
Avatar,
},
props: {
balance: { type: Number, required: true },
},

View File

@ -1,9 +1,10 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { nextTick, ref } from 'vue'
import SessionLogoutTimeout from './SessionLogoutTimeout.vue'
import { useLazyQuery } from '@vue/apollo-composable'
import { useStore } from 'vuex'
import { useModal } from 'bootstrap-vue-next'
// Mock external dependencies
vi.mock('vuex', () => ({
@ -13,17 +14,15 @@ vi.mock('vuex', () => ({
vi.mock('@vue/apollo-composable', () => ({
useLazyQuery: vi.fn(() => ({
load: vi.fn(),
loading: false,
error: { value: null },
loading: ref(false),
error: ref(null),
})),
}))
// Mock bootstrap-vue-next
const mockShow = vi.fn()
const mockHide = vi.fn()
vi.mock('bootstrap-vue-next', () => ({
useModal: vi.fn(() => ({
show: mockShow,
hide: mockHide,
})),
BModal: { template: '<div><slot></slot><slot name="modal-footer"></slot></div>' },
@ -59,12 +58,12 @@ describe('SessionLogoutTimeout', () => {
beforeEach(() => {
vi.useFakeTimers()
mockShow.mockClear()
mockHide.mockClear()
})
afterEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
})
it('renders the component div.session-logout-timeout', () => {
@ -72,69 +71,115 @@ describe('SessionLogoutTimeout', () => {
expect(wrapper.find('div.session-logout-timeout').exists()).toBe(true)
})
describe('tokenExpiresInSeconds computed property', () => {
it('returns 0 when token is expired', async () => {
wrapper = createWrapper(setTokenTime(-60))
await nextTick()
expect(wrapper.vm.tokenExpiresInSeconds).toBe(0)
})
it('returns remaining seconds when token is not expired', async () => {
wrapper = createWrapper(setTokenTime(120))
await nextTick()
expect(wrapper.vm.tokenExpiresInSeconds).toBeGreaterThan(0)
expect(wrapper.vm.tokenExpiresInSeconds).toBeLessThanOrEqual(120)
})
})
describe('checkExpiration', () => {
it('shows modal when token expires in less than 75 seconds', async () => {
describe('token expiration', () => {
it('shows modal when remaining time is 75 seconds or less', async () => {
wrapper = createWrapper(setTokenTime(74))
await nextTick()
vi.runAllTimers()
vi.runOnlyPendingTimers()
await nextTick()
expect(mockShow).toHaveBeenCalled()
const modal = wrapper.findComponent({ name: 'BModal' })
expect(modal.props('modelValue')).toBe(true)
})
it('emits logout when token is expired', async () => {
wrapper = createWrapper(setTokenTime(-1))
it('does not show modal when remaining time is more than 75 seconds', async () => {
wrapper = createWrapper(setTokenTime(76))
await nextTick()
vi.runOnlyPendingTimers()
await nextTick()
const modal = wrapper.findComponent({ name: 'BModal' })
expect(modal.props('modelValue')).toBe(false)
})
it('emits logout when time expires', async () => {
wrapper = createWrapper(setTokenTime(1))
await nextTick()
vi.runAllTimers()
await nextTick()
expect(wrapper.emitted('logout')).toBeTruthy()
})
})
describe('handleOk', () => {
it('hides modal and does not emit logout on successful verification', async () => {
it('hides modal and continues session on successful verification', async () => {
const mockLoad = vi.fn().mockResolvedValue({})
vi.mocked(useLazyQuery).mockReturnValue({
load: mockLoad,
loading: false,
error: { value: null },
loading: ref(false),
error: ref(null),
})
wrapper = createWrapper()
await wrapper.vm.handleOk({ preventDefault: vi.fn() })
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
await nextTick()
expect(mockLoad).toHaveBeenCalled()
expect(mockHide).toHaveBeenCalledWith('modalSessionTimeOut')
expect(wrapper.emitted('logout')).toBeFalsy()
})
it('emits logout on failed verification', async () => {
const mockLoad = vi.fn().mockRejectedValue(new Error('Verification failed'))
it('emits logout on verification failure', async () => {
const mockLoad = vi.fn().mockResolvedValue({})
vi.mocked(useLazyQuery).mockReturnValue({
load: mockLoad,
loading: false,
error: { value: new Error('Verification failed') },
loading: ref(false),
error: ref(new Error('Verification failed')),
})
wrapper = createWrapper()
await wrapper.vm.handleOk({ preventDefault: vi.fn() })
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
await nextTick()
expect(mockLoad).toHaveBeenCalled()
expect(wrapper.emitted('logout')).toBeTruthy()
})
it('emits logout when verification throws an error', async () => {
const mockLoad = vi.fn().mockRejectedValue(new Error('Network error'))
vi.mocked(useLazyQuery).mockReturnValue({
load: mockLoad,
loading: ref(false),
error: ref(null),
})
wrapper = createWrapper()
await wrapper.findComponent({ name: 'BButton' }).trigger('click')
await nextTick()
expect(mockLoad).toHaveBeenCalled()
expect(wrapper.emitted('logout')).toBeTruthy()
})
})
describe('time formatting', () => {
it('formats remaining time correctly', async () => {
wrapper = createWrapper(setTokenTime(65))
await nextTick()
const warningText = wrapper.find('.text-warning')
expect(warningText.text()).toContain('65')
})
it('shows 00 when time is expired', async () => {
wrapper = createWrapper(setTokenTime(-1))
await nextTick()
const warningText = wrapper.find('.text-warning')
expect(warningText.text()).toContain('00')
})
})
describe('cleanup', () => {
it('clears interval on unmount', () => {
const clearIntervalSpy = vi.spyOn(window, 'clearInterval')
wrapper = createWrapper()
wrapper.unmount()
expect(clearIntervalSpy).toHaveBeenCalled()
})
})
})

View File

@ -54,6 +54,7 @@ export default {
font-size: 14px;
text-wrap: nowrap;
color: black !important;
border-radius: 25px;
}
}

View File

@ -8,12 +8,20 @@
<BRow align-v="center" class="mb-4">
<BCol cols="auto">
<div class="align-items-center">
<avatar
<!-- <avatar-->
<!-- class="vue3-avatar"-->
<!-- :size="72"-->
<!-- :color="'#fff'"-->
<!-- :name="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"-->
<!-- :initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"-->
<!-- :border="false"-->
<!-- />-->
<app-avatar
:size="72"
:color="'#fff'"
:username="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
:name="`${transaction.linkedUser.firstName} ${transaction.linkedUser.lastName}`"
:initials="`${transaction.linkedUser.firstName[0]}${transaction.linkedUser.lastName[0]}`"
></avatar>
/>
</div>
</BCol>
<BCol class="p-1">
@ -43,11 +51,11 @@
</div>
</template>
<script setup>
import Avatar from 'vue-avatar'
import Name from '@/components/TransactionRows/Name'
import { useRoute, useRouter } from 'vue-router'
import { useStore } from 'vuex'
import { computed } from 'vue'
import AppAvatar from '@/components/AppAvatar.vue'
const props = defineProps({
transactions: {
default: () => [],

View File

@ -60,11 +60,11 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { useStore } from 'vuex'
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import Name from '../TransactionRows/Name'
import DecayInformation from '../DecayInformations/DecayInformation'
import { BAvatar, BRow } from 'bootstrap-vue-next'
import AppAvatar from '@/components/AppAvatar.vue'
const props = defineProps({
transaction: {
@ -92,7 +92,7 @@ const isCreationType = computed(() => {
})
const avatarComponent = computed(() => {
return isCreationType.value ? BAvatar : Avatar
return isCreationType.value ? BAvatar : AppAvatar
})
const avatarProps = computed(() => {

View File

@ -1,16 +1,16 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import TransactionReceive from './TransactionReceive'
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import Name from '../TransactionRows/Name'
import DecayInformation from '../DecayInformations/DecayInformation'
import { BCol, BCollapse, BRow } from 'bootstrap-vue-next'
import AppAvatar from '@/components/AppAvatar.vue'
// Mock child components
vi.mock('vue-avatar', () => ({
vi.mock('app-avatar', () => ({
default: {
name: 'Avatar',
name: 'AppAvatar',
render: () => null,
},
}))
@ -86,7 +86,7 @@ describe('TransactionReceive', () => {
$d: (date, format) => `Mocked ${format} date for ${date}`,
},
components: {
Avatar,
AppAvatar,
CollapseIcon,
Name,
DecayInformation,

View File

@ -3,12 +3,14 @@
<BRow class="align-items-center">
<BCol cols="3" lg="2" md="2">
<!-- <b-avatar :text="avatarText" variant="success" size="3em"></b-avatar> -->
<avatar
:username="username.username"
<app-avatar
class="vue3-avatar"
:name="username.username"
:initials="username.initials"
:color="'#fff'"
:size="42"
></avatar>
:border="false"
/>
</BCol>
<BCol>
<div>
@ -46,7 +48,6 @@
</div>
</template>
<script>
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import Name from '../TransactionRows/Name'
import DecayInformation from '../DecayInformations/DecayInformation'
@ -54,7 +55,6 @@ import DecayInformation from '../DecayInformations/DecayInformation'
export default {
name: 'TransactionReceive',
components: {
Avatar,
CollapseIcon,
Name,
DecayInformation,

View File

@ -1,15 +1,15 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import TransactionSend from './TransactionSend'
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import Name from '../TransactionRows/Name'
import DecayInformation from '../DecayInformations/DecayInformation'
import { BCol, BCollapse, BRow } from 'bootstrap-vue-next'
import AppAvatar from '@/components/AppAvatar.vue'
vi.mock('vue-avatar', () => ({
vi.mock('app-avatar', () => ({
default: {
name: 'Avatar',
name: 'AppAvatar',
render: () => null,
},
}))
@ -84,7 +84,7 @@ describe('TransactionSend', () => {
$d: (date, format) => `Mocked ${format} date for ${date}`,
},
components: {
Avatar,
AppAvatar,
CollapseIcon,
Name,
DecayInformation,

View File

@ -2,11 +2,13 @@
<div class="transaction-slot-send" @click="visible = !visible">
<BRow class="align-items-center">
<BCol cols="3" lg="2" md="2">
<avatar
:username="username.username"
<app-avatar
class="vue3-avatar"
:name="username.username"
:initials="username.initials"
:color="'#fff'"
:size="42"
:border="false"
/>
</BCol>
<BCol>
@ -45,7 +47,6 @@
</div>
</template>
<script>
import Avatar from 'vue-avatar'
import CollapseIcon from '../TransactionRows/CollapseIcon'
import Name from '../TransactionRows/Name'
import DecayInformation from '../DecayInformations/DecayInformation'
@ -53,7 +54,6 @@ import DecayInformation from '../DecayInformations/DecayInformation'
export default {
name: 'TransactionSend',
components: {
Avatar,
CollapseIcon,
Name,
DecayInformation,

View File

@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import UserCard from './UserCard.vue'
import { BCol, BRow } from 'bootstrap-vue-next'
import AppAvatar from '@/components/AppAvatar.vue'
vi.mock('vue-avatar', () => ({
default: {
@ -39,6 +40,7 @@ describe('UserCard', () => {
components: {
BRow,
BCol,
AppAvatar,
},
mocks,
},
@ -53,12 +55,12 @@ describe('UserCard', () => {
expect(wrapper.find('.userdata-card').exists()).toBe(true)
})
it('renders the SPAN Element ".vue-avatar--wrapper"', () => {
expect(wrapper.find('.vue-avatar--wrapper').exists()).toBe(true)
it('renders the DIV Element ".app-avatar"', () => {
expect(wrapper.find('.app-avatar').exists()).toBe(true)
})
it('displays the first letters of the firstName and lastName', () => {
expect(wrapper.find('.vue-avatar--wrapper span').text()).toBe('BB')
expect(wrapper.find('.app-avatar').text()).toBe('BB')
})
it('displays the correct balance', () => {

View File

@ -1,12 +1,14 @@
<template>
<div class="userdata-card">
<div class="center-per-margin">
<avatar
:username="username.username"
<app-avatar
class="vue3-avatar"
:name="username.username"
:initials="username.initials"
:color="'#fff'"
:size="90"
></avatar>
:border="false"
/>
</div>
<div class="justify-content-center mt-5 mb-5">
@ -44,14 +46,10 @@
</div>
</template>
<script>
import Avatar from 'vue-avatar'
import CONFIG from '@/config'
export default {
name: 'UserCard',
components: {
Avatar,
},
props: {
balance: { type: Number, default: 0 },
transactionCount: { type: Number, default: 0 },

View File

@ -4,8 +4,10 @@ import UserNewsletter from './UserNewsletter'
import { unsubscribeNewsletter, subscribeNewsletter } from '@/graphql/mutations'
import { createStore } from 'vuex'
import { createI18n } from 'vue-i18n'
import { BFormCheckbox } from 'bootstrap-vue-next'
import * as bootstrapVueNext from 'bootstrap-vue-next'
import { nextTick } from 'vue'
// Mock composables and dependencies
const mockToastError = vi.fn()
const mockToastSuccess = vi.fn()
@ -34,7 +36,7 @@ describe('UserNewsletter', () => {
let store
let i18n
const createVuexStore = (initialState) =>
const createVuexStore = (initialState = {}) =>
createStore({
state: {
language: 'de',
@ -42,7 +44,7 @@ describe('UserNewsletter', () => {
...initialState,
},
mutations: {
setNewsletterState(state, value) {
newsletterState(state, value) {
state.newsletterState = value
},
},
@ -63,11 +65,14 @@ describe('UserNewsletter', () => {
const createWrapper = (storeState = {}) => {
store = createVuexStore(storeState)
i18n = createI18nInstance()
return mount(UserNewsletter, {
global: {
plugins: [store, i18n],
stubs: {
BFormCheckbox: true,
plugins: [store, i18n, bootstrapVueNext],
config: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('b-'),
},
},
},
})
@ -82,8 +87,9 @@ describe('UserNewsletter', () => {
expect(wrapper.find('div.formusernewsletter').exists()).toBe(true)
})
it('has an edit BFormCheckbox switch', () => {
expect(wrapper.find('[test="BFormCheckbox"]').exists()).toBe(true)
it('has a BFormCheckbox switch', () => {
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
expect(checkbox.exists()).toBe(true)
})
describe('unsubscribe with success', () => {
@ -94,7 +100,12 @@ describe('UserNewsletter', () => {
unsubscribeNewsletter: true,
},
})
wrapper.vm.localNewsletterState = false
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
await checkbox.setValue(false)
await nextTick()
// Ensure all promises are resolved
await Promise.resolve()
})
it('calls the unsubscribe mutation', () => {
@ -118,7 +129,12 @@ describe('UserNewsletter', () => {
subscribeNewsletter: true,
},
})
wrapper.vm.localNewsletterState = true
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
await checkbox.setValue(true)
await nextTick()
// Ensure all promises are resolved
await Promise.resolve()
})
it('calls the subscribe mutation', () => {
@ -138,7 +154,12 @@ describe('UserNewsletter', () => {
beforeEach(async () => {
wrapper = createWrapper({ newsletterState: true })
mockUnsubscribeMutate.mockRejectedValue(new Error('Ouch'))
wrapper.vm.localNewsletterState = false
const checkbox = wrapper.findComponent({ name: 'BFormCheckbox' })
await checkbox.setValue(false)
await nextTick()
// Ensure all promises are resolved
await Promise.resolve()
})
it('resets the newsletterState', () => {

View File

@ -36,13 +36,9 @@ watch(localNewsletterState, async (newValue, oldValue) => {
const onSubmit = async () => {
try {
if (localNewsletterState.value) {
await newsletterSubscribe()
} else {
await newsletterUnsubscribe()
}
localNewsletterState.value ? await newsletterSubscribe() : await newsletterUnsubscribe()
store.commit('setNewsletterState', localNewsletterState.value)
store.commit('newsletterState', localNewsletterState.value)
toastSuccess(
localNewsletterState.value

View File

@ -19,13 +19,13 @@ export default defineConfig({
},
resolve: {
alias: {
vue: '@vue/compat',
'@': path.resolve(__dirname, './src'),
assets: path.join(__dirname, 'src/assets'),
},
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
plugins: [
vue(),
createHtmlPlugin({
minify: true,
inject: {
@ -41,15 +41,6 @@ export default defineConfig({
},
},
}),
vue({
template: {
compilerOptions: {
compatConfig: {
MODE: 2,
},
},
},
}),
Components({
resolvers: [BootstrapVueNextResolver(), IconsResolve()],
dts: true,

View File

@ -1756,10 +1756,10 @@
vee-validate "4.13.2"
yup "^1.3.2"
"@vitejs/plugin-vue@3.2.0":
version "3.2.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-3.2.0.tgz#a1484089dd85d6528f435743f84cdd0d215bbb54"
integrity sha512-E0tnaL4fr+qkdCNxJ+Xd0yM31UwMkQje76fsDVBBUCoGOUPexu2VDUYHL8P4CwV+zMvWw6nlRw19OnRKmYAJpw==
"@vitejs/plugin-vue@5.1.4":
version "5.1.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.4.tgz#72b8b705cfce36b00b59af196195146e356500c4"
integrity sha512-N2XSI2n3sQqp5w7Y/AN/L2XDjBIRGqXko+eDp42sydYSBeJuSm5a1sLf8zakmo8u7tA8NmBgoDLA1HeOESjp9A==
"@vitest/coverage-v8@^2.0.5":
version "2.0.5"
@ -1853,15 +1853,6 @@
dependencies:
throttle-debounce "^5.0.0"
"@vue/compat@^3.4.31":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/compat/-/compat-3.4.36.tgz#9af3b95adf614354f2595548f162e7af4891aba9"
integrity sha512-41qHnPjRAjhNGCR77BQeaNKYNyZNqtZDNgg/3LQ3zIfOA8+cqFFPzZ/Ym1K4Viin0RS7bN4C8/fQFaRrvy7lnA==
dependencies:
"@babel/parser" "^7.24.7"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-core@3.4.31":
version "3.4.31"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.31.tgz#b51a76f1b30e9b5eba0553264dff0f171aedb7c6"
@ -1873,17 +1864,6 @@
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-core@3.4.36":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.36.tgz#4e28dfcbaa8a85e135f7a94c44372b6d52329e42"
integrity sha512-qBkndgpwFKdupmOPoiS10i7oFdN7a+4UNDlezD0GlQ1kuA1pNrscg9g12HnB5E8hrWSuEftRsbJhL1HI2zpJhg==
dependencies:
"@babel/parser" "^7.24.7"
"@vue/shared" "3.4.36"
entities "^5.0.0"
estree-walker "^2.0.2"
source-map-js "^1.2.0"
"@vue/compiler-core@3.5.12":
version "3.5.12"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.12.tgz#bd70b7dabd12b0b6f31bc53418ba3da77994c437"
@ -1903,14 +1883,6 @@
"@vue/compiler-core" "3.4.31"
"@vue/shared" "3.4.31"
"@vue/compiler-dom@3.4.36":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.36.tgz#32f5f65d1fb242211df2ddc65a336779cd8b974c"
integrity sha512-eEIjy4GwwZTFon/Y+WO8tRRNGqylaRlA79T1RLhUpkOzJ7EtZkkb8MurNfkqY6x6Qiu0R7ESspEF7GkPR/4yYg==
dependencies:
"@vue/compiler-core" "3.4.36"
"@vue/shared" "3.4.36"
"@vue/compiler-dom@3.5.12":
version "3.5.12"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.12.tgz#456d631d11102535b7ee6fd954cf2c93158d0354"
@ -1949,21 +1921,6 @@
postcss "^8.4.47"
source-map-js "^1.2.0"
"@vue/compiler-sfc@^3.4.35":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.36.tgz#887809183a273dc0ef8337d5e84ef6a781727ccc"
integrity sha512-rhuHu7qztt/rNH90dXPTzhB7hLQT2OC4s4GrPVqmzVgPY4XBlfWmcWzn4bIPEWNImt0CjO7kfHAf/1UXOtx3vw==
dependencies:
"@babel/parser" "^7.24.7"
"@vue/compiler-core" "3.4.36"
"@vue/compiler-dom" "3.4.36"
"@vue/compiler-ssr" "3.4.36"
"@vue/shared" "3.4.36"
estree-walker "^2.0.2"
magic-string "^0.30.10"
postcss "^8.4.40"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.4.31":
version "3.4.31"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz#f62ffecdf15bacb883d0099780cf9a1e3654bfc4"
@ -1972,14 +1929,6 @@
"@vue/compiler-dom" "3.4.31"
"@vue/shared" "3.4.31"
"@vue/compiler-ssr@3.4.36":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.36.tgz#5881f9303ad6a4fdf04fb4238ebb483caf040707"
integrity sha512-Wt1zyheF0zVvRJyhY74uxQbnkXV2Le/JPOrAxooR4rFYKC7cFr+cRqW6RU3cM/bsTy7sdZ83IDuy/gLPSfPGng==
dependencies:
"@vue/compiler-dom" "3.4.36"
"@vue/shared" "3.4.36"
"@vue/compiler-ssr@3.5.12":
version "3.5.12"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.12.tgz#5f1a3fbd5c44b79a6dbe88729f7801d9c9218bde"
@ -2073,11 +2022,6 @@
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.31.tgz#af9981f57def2c3f080c14bf219314fc0dc808a0"
integrity sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA==
"@vue/shared@3.4.36":
version "3.4.36"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.36.tgz#7551f41684966acb6a307152b49a8308e7f69203"
integrity sha512-fdPLStwl1sDfYuUftBaUVn2pIrVFDASYerZSrlBvVBfylObPA1gtcWJHy5Ox8jLEJ524zBibss488Q3SZtU1uA==
"@vue/shared@3.5.12":
version "3.5.12"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.12.tgz#f9e45b7f63f2c3f40d84237b1194b7f67de192e3"
@ -3312,11 +3256,6 @@ entities@^4.2.0, entities@^4.4.0, entities@^4.5.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-5.0.0.tgz#b2ab51fe40d995817979ec79dd621154c3c0f62b"
integrity sha512-BeJFvFRJddxobhvEdm5GqHzRV/X+ACeuw0/BuuxsCh1EUZcAIz8+kYmBp/LrQuloy6K1f3a0M7+IhmZ7QnkISA==
env-paths@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
@ -5927,7 +5866,7 @@ postcss-value-parser@^4.2.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.0, postcss@^8.4.18, postcss@^8.4.38, postcss@^8.4.39, postcss@^8.4.40, postcss@^8.4.8:
postcss@^8.4.0, postcss@^8.4.18, postcss@^8.4.38, postcss@^8.4.39, postcss@^8.4.8:
version "8.4.41"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.41.tgz#d6104d3ba272d882fe18fc07d15dc2da62fa2681"
integrity sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==
@ -7335,11 +7274,6 @@ vue-apollo@^3.1.2:
serialize-javascript "^4.0.0"
throttle-debounce "^2.1.0"
vue-avatar@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/vue-avatar/-/vue-avatar-2.3.3.tgz#e125bf4f4a6f4f9480da0c522020266a8609d2a8"
integrity sha512-Z57ILRTkFIAuCH9JiFBxX74C5zua5ub/jRDM/KZ+QKXNfscvmUOgWBs3kA2+wrpZMowIvfLHIT0gvQu1z+zpLg==
vue-component-type-helpers@^2.0.0:
version "2.1.6"
resolved "https://registry.yarnpkg.com/vue-component-type-helpers/-/vue-component-type-helpers-2.1.6.tgz#f350515b252ed9e76960ac51f135636f8baef3fe"
@ -7401,6 +7335,11 @@ vue-timers@^2.0.4:
resolved "https://registry.yarnpkg.com/vue-timers/-/vue-timers-2.0.4.tgz#7e1c443abf2109db5eeab6e62b0f5a47e94cf70b"
integrity sha512-QOEVdO4V4o9WjFG6C0Kn9tfdTeeECjqvEQozcQlfL1Tn8v0qx4uUPhTYoc1+s6qoJnSbu8f68x8+nm1ZEir0kw==
vue3-avatar@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vue3-avatar/-/vue3-avatar-3.1.0.tgz#0941fcd0ea450868af6a70f9aa7ee033467f7d13"
integrity sha512-K1h98jJckc3WksZsme80CHxTLDU7aMSOujrhPGFcJ5Yxoq3I+cqgbe4wUh52G8vM65JbxfbS+BmCcladg0o6GA==
vue@3.4.31:
version "3.4.31"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.31.tgz#83a3c4dab8302b0e974b0d4b92a2f6a6378ae797"