mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
fix(webapp): added timer (#8658)
This commit is contained in:
parent
915091286a
commit
2b457a5823
@ -15,8 +15,9 @@
|
|||||||
:show-avatar="showAvatar"
|
:show-avatar="showAvatar"
|
||||||
:date-time="dateTime"
|
:date-time="dateTime"
|
||||||
:show-popover="showPopover"
|
:show-popover="showPopover"
|
||||||
:injectedText="injectedText"
|
:injected-text="injectedText"
|
||||||
:injectedDate="injectedDate"
|
:injected-date="injectedDate"
|
||||||
|
:hover-delay="hoverDelay"
|
||||||
@close="closeMenu"
|
@close="closeMenu"
|
||||||
/>
|
/>
|
||||||
</client-only>
|
</client-only>
|
||||||
@ -45,6 +46,7 @@ export default {
|
|||||||
showPopover: { type: Boolean, default: true },
|
showPopover: { type: Boolean, default: true },
|
||||||
injectedText: { type: String, default: null },
|
injectedText: { type: String, default: null },
|
||||||
injectedDate: { type: Boolean, default: false },
|
injectedDate: { type: Boolean, default: false },
|
||||||
|
hoverDelay: { type: Number, default: 500 },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { render } from '@testing-library/vue'
|
import { render, waitFor, fireEvent } from '@testing-library/vue'
|
||||||
import { RouterLinkStub } from '@vue/test-utils'
|
import { RouterLinkStub } from '@vue/test-utils'
|
||||||
import UserTeaserHelper from './UserTeaserHelper.vue'
|
import UserTeaserHelper from './UserTeaserHelper.vue'
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ describe('UserTeaserHelper', () => {
|
|||||||
withLinkToProfile = true,
|
withLinkToProfile = true,
|
||||||
onTouchScreen = false,
|
onTouchScreen = false,
|
||||||
withPopoverEnabled = true,
|
withPopoverEnabled = true,
|
||||||
|
hoverDelay = 500,
|
||||||
}) => {
|
}) => {
|
||||||
mockIsTouchDevice = onTouchScreen
|
mockIsTouchDevice = onTouchScreen
|
||||||
|
|
||||||
@ -29,13 +30,109 @@ describe('UserTeaserHelper', () => {
|
|||||||
userLink,
|
userLink,
|
||||||
linkToProfile: withLinkToProfile,
|
linkToProfile: withLinkToProfile,
|
||||||
showPopover: withPopoverEnabled,
|
showPopover: withPopoverEnabled,
|
||||||
|
hoverDelay: hoverDelay,
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
NuxtLink: RouterLinkStub,
|
NuxtLink: RouterLinkStub,
|
||||||
},
|
},
|
||||||
|
slots: {
|
||||||
|
default: '<div>Test Content</div>',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for tests that need timers
|
||||||
|
const withFakeTimers = (testFn) => {
|
||||||
|
return async () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
try {
|
||||||
|
await testFn()
|
||||||
|
} finally {
|
||||||
|
jest.runOnlyPendingTimers()
|
||||||
|
jest.useRealTimers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('hover delay functionality', () => {
|
||||||
|
it(
|
||||||
|
'should emit open-menu after hover delay on desktop',
|
||||||
|
withFakeTimers(async () => {
|
||||||
|
const wrapper = Wrapper({
|
||||||
|
withLinkToProfile: true,
|
||||||
|
onTouchScreen: false,
|
||||||
|
withPopoverEnabled: true,
|
||||||
|
hoverDelay: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Find the NuxtLink stub element
|
||||||
|
const link = wrapper.container.firstChild
|
||||||
|
expect(link).toBeTruthy()
|
||||||
|
|
||||||
|
// Trigger mouseenter
|
||||||
|
fireEvent.mouseEnter(link)
|
||||||
|
|
||||||
|
// Menu should not be opened yet
|
||||||
|
expect(wrapper.emitted()['open-menu']).toBeFalsy()
|
||||||
|
|
||||||
|
// Advance time
|
||||||
|
jest.advanceTimersByTime(1000)
|
||||||
|
|
||||||
|
// Now open-menu should have been emitted
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(wrapper.emitted()['open-menu']).toBeTruthy()
|
||||||
|
expect(wrapper.emitted()['open-menu']).toHaveLength(1)
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it(
|
||||||
|
'should not emit open-menu if mouse leaves before delay',
|
||||||
|
withFakeTimers(async () => {
|
||||||
|
const wrapper = Wrapper({
|
||||||
|
withLinkToProfile: true,
|
||||||
|
onTouchScreen: false,
|
||||||
|
withPopoverEnabled: true,
|
||||||
|
hoverDelay: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const link = wrapper.container.firstChild
|
||||||
|
|
||||||
|
// Mouseenter + mouseleave
|
||||||
|
fireEvent.mouseEnter(link)
|
||||||
|
jest.advanceTimersByTime(500)
|
||||||
|
fireEvent.mouseLeave(link)
|
||||||
|
|
||||||
|
// Let the rest of the time pass
|
||||||
|
jest.advanceTimersByTime(500)
|
||||||
|
|
||||||
|
// open-menu should not have been emitted
|
||||||
|
expect(wrapper.emitted()['open-menu']).toBeFalsy()
|
||||||
|
// But close-menu should have been emitted
|
||||||
|
expect(wrapper.emitted()['close-menu']).toBeTruthy()
|
||||||
|
expect(wrapper.emitted()['close-menu']).toHaveLength(1)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
it('should emit open-menu immediately on touch device button click', async () => {
|
||||||
|
const wrapper = Wrapper({
|
||||||
|
withLinkToProfile: true,
|
||||||
|
onTouchScreen: true,
|
||||||
|
withPopoverEnabled: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const button = wrapper.container.querySelector('button')
|
||||||
|
expect(button).toBeTruthy()
|
||||||
|
|
||||||
|
// Click on button
|
||||||
|
fireEvent.click(button)
|
||||||
|
|
||||||
|
// Should be emitted immediately, without timer
|
||||||
|
expect(wrapper.emitted()['open-menu']).toBeTruthy()
|
||||||
|
expect(wrapper.emitted()['open-menu']).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('with linkToProfile and popover enabled, on touch screen', () => {
|
describe('with linkToProfile and popover enabled, on touch screen', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -43,6 +140,7 @@ describe('UserTeaserHelper', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders button', () => {
|
it('renders button', () => {
|
||||||
|
expect(wrapper.container.querySelector('button')).toBeTruthy()
|
||||||
expect(wrapper.container).toMatchSnapshot()
|
expect(wrapper.container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -54,6 +152,7 @@ describe('UserTeaserHelper', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders span', () => {
|
it('renders span', () => {
|
||||||
|
expect(wrapper.container.querySelector('span')).toBeTruthy()
|
||||||
expect(wrapper.container).toMatchSnapshot()
|
expect(wrapper.container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -65,7 +164,55 @@ describe('UserTeaserHelper', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('renders link', () => {
|
it('renders link', () => {
|
||||||
|
const routerLinkStub = wrapper.container.firstChild
|
||||||
|
expect(routerLinkStub).toBeTruthy()
|
||||||
|
expect(routerLinkStub.tagName).toBeTruthy()
|
||||||
expect(wrapper.container).toMatchSnapshot()
|
expect(wrapper.container).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('timer cleanup', () => {
|
||||||
|
it(
|
||||||
|
'should clear timer on component unmount',
|
||||||
|
withFakeTimers(async () => {
|
||||||
|
const clearTimeoutSpy = jest.spyOn(global, 'clearTimeout')
|
||||||
|
|
||||||
|
const wrapper = Wrapper({
|
||||||
|
withLinkToProfile: true,
|
||||||
|
onTouchScreen: false,
|
||||||
|
withPopoverEnabled: true,
|
||||||
|
hoverDelay: 5000,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start hover timer
|
||||||
|
fireEvent.mouseEnter(wrapper.container.firstChild)
|
||||||
|
|
||||||
|
// Unmount while timer is running
|
||||||
|
wrapper.unmount()
|
||||||
|
|
||||||
|
// clearTimeout should have been called
|
||||||
|
expect(clearTimeoutSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
clearTimeoutSpy.mockRestore()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('popover behavior without showPopover', () => {
|
||||||
|
it('should not emit events when showPopover is false', () => {
|
||||||
|
const wrapper = Wrapper({
|
||||||
|
withLinkToProfile: true,
|
||||||
|
onTouchScreen: false,
|
||||||
|
withPopoverEnabled: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const link = wrapper.container.firstChild
|
||||||
|
|
||||||
|
fireEvent.mouseEnter(link)
|
||||||
|
fireEvent.mouseLeave(link)
|
||||||
|
|
||||||
|
expect(wrapper.emitted()['open-menu']).toBeUndefined()
|
||||||
|
expect(wrapper.emitted()['close-menu']).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,16 +4,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-else-if="!linkToProfile || !userLink"
|
v-else-if="!linkToProfile || !userLink"
|
||||||
@mouseover="() => showPopover && openMenu()"
|
@mouseenter="handleMouseEnter"
|
||||||
@mouseleave="closeMenu"
|
@mouseleave="handleMouseLeave"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
v-else
|
v-else
|
||||||
:to="userLink"
|
:to="userLink"
|
||||||
@mouseover.native="() => showPopover && openMenu()"
|
@mouseenter.native="handleMouseEnter"
|
||||||
@mouseleave.native="closeMenu"
|
@mouseleave.native="handleMouseLeave"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@ -28,6 +28,13 @@ export default {
|
|||||||
userLink: { type: Object, default: null },
|
userLink: { type: Object, default: null },
|
||||||
linkToProfile: { type: Boolean, default: true },
|
linkToProfile: { type: Boolean, default: true },
|
||||||
showPopover: { type: Boolean, default: false },
|
showPopover: { type: Boolean, default: false },
|
||||||
|
hoverDelay: { type: Number, default: 500 },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hoverTimer: null,
|
||||||
|
isHovering: false,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isTouchDevice() {
|
isTouchDevice() {
|
||||||
@ -35,12 +42,46 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleMouseEnter() {
|
||||||
|
if (!this.showPopover) return
|
||||||
|
|
||||||
|
this.isHovering = true
|
||||||
|
|
||||||
|
this.clearHoverTimer()
|
||||||
|
this.hoverTimer = setTimeout(() => {
|
||||||
|
// Only open if still hovering
|
||||||
|
if (this.isHovering) {
|
||||||
|
this.openMenu()
|
||||||
|
}
|
||||||
|
}, this.hoverDelay)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMouseLeave() {
|
||||||
|
if (!this.showPopover) return
|
||||||
|
|
||||||
|
this.isHovering = false
|
||||||
|
this.clearHoverTimer()
|
||||||
|
this.closeMenu()
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHoverTimer() {
|
||||||
|
if (this.hoverTimer) {
|
||||||
|
clearTimeout(this.hoverTimer)
|
||||||
|
this.hoverTimer = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openMenu() {
|
openMenu() {
|
||||||
this.$emit('open-menu')
|
this.$emit('open-menu')
|
||||||
},
|
},
|
||||||
|
|
||||||
closeMenu() {
|
closeMenu() {
|
||||||
this.$emit('close-menu')
|
this.$emit('close-menu')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
this.clearHoverTimer()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
:link-to-profile="linkToProfile"
|
:link-to-profile="linkToProfile"
|
||||||
:show-popover="showPopover"
|
:show-popover="showPopover"
|
||||||
:user-link="userLink"
|
:user-link="userLink"
|
||||||
|
:hover-delay="hoverDelay"
|
||||||
@open-menu="loadPopover(openMenu)"
|
@open-menu="loadPopover(openMenu)"
|
||||||
@close-menu="closeMenu(false)"
|
@close-menu="closeMenu(false)"
|
||||||
data-test="avatarUserLink"
|
data-test="avatarUserLink"
|
||||||
@ -18,6 +19,7 @@
|
|||||||
:link-to-profile="linkToProfile"
|
:link-to-profile="linkToProfile"
|
||||||
:show-popover="showPopover"
|
:show-popover="showPopover"
|
||||||
:user-link="userLink"
|
:user-link="userLink"
|
||||||
|
:hover-delay="hoverDelay"
|
||||||
@open-menu="loadPopover(openMenu)"
|
@open-menu="loadPopover(openMenu)"
|
||||||
@close-menu="closeMenu(false)"
|
@close-menu="closeMenu(false)"
|
||||||
>
|
>
|
||||||
@ -67,7 +69,7 @@ import UserTeaserPopover from './UserTeaserPopover'
|
|||||||
import UserTeaserHelper from './UserTeaserHelper.vue'
|
import UserTeaserHelper from './UserTeaserHelper.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'UserTeaser',
|
name: 'UserTeaserNonAnonymous',
|
||||||
components: {
|
components: {
|
||||||
ProfileAvatar,
|
ProfileAvatar,
|
||||||
UserTeaserPopover,
|
UserTeaserPopover,
|
||||||
@ -85,6 +87,7 @@ export default {
|
|||||||
showPopover: { type: Boolean, default: true },
|
showPopover: { type: Boolean, default: true },
|
||||||
injectedText: { type: String, default: null },
|
injectedText: { type: String, default: null },
|
||||||
injectedDate: { type: Boolean, default: false },
|
injectedDate: { type: Boolean, default: false },
|
||||||
|
hoverDelay: { type: Number, default: 500 },
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
|
|||||||
@ -353,7 +353,6 @@ exports[`UserTeaser given an user with linkToProfile, on desktop when hovering t
|
|||||||
delay="0"
|
delay="0"
|
||||||
handleresize="true"
|
handleresize="true"
|
||||||
offset="16"
|
offset="16"
|
||||||
open="true"
|
|
||||||
openclass="open"
|
openclass="open"
|
||||||
opengroup="0"
|
opengroup="0"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@ -421,12 +420,7 @@ exports[`UserTeaser given an user with linkToProfile, on desktop when hovering t
|
|||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div />
|
||||||
<user-teaser-popover-stub
|
|
||||||
user-id="user1"
|
|
||||||
user-link="[object Object]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-popover-stub>
|
</v-popover-stub>
|
||||||
</client-only-stub>
|
</client-only-stub>
|
||||||
</div>
|
</div>
|
||||||
@ -717,7 +711,6 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
delay="0"
|
delay="0"
|
||||||
handleresize="true"
|
handleresize="true"
|
||||||
offset="16"
|
offset="16"
|
||||||
open="true"
|
|
||||||
openclass="open"
|
openclass="open"
|
||||||
opengroup="0"
|
opengroup="0"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@ -785,11 +778,7 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div />
|
||||||
<user-teaser-popover-stub
|
|
||||||
user-id="user1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-popover-stub>
|
</v-popover-stub>
|
||||||
</client-only-stub>
|
</client-only-stub>
|
||||||
</div>
|
</div>
|
||||||
@ -810,7 +799,6 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
delay="0"
|
delay="0"
|
||||||
handleresize="true"
|
handleresize="true"
|
||||||
offset="16"
|
offset="16"
|
||||||
open="true"
|
|
||||||
openclass="open"
|
openclass="open"
|
||||||
opengroup="0"
|
opengroup="0"
|
||||||
placement="bottom-end"
|
placement="bottom-end"
|
||||||
@ -878,11 +866,7 @@ exports[`UserTeaser given an user without linkToProfile, on desktop when hoverin
|
|||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div />
|
||||||
<user-teaser-popover-stub
|
|
||||||
user-id="user1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-popover-stub>
|
</v-popover-stub>
|
||||||
</client-only-stub>
|
</client-only-stub>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,18 +2,30 @@
|
|||||||
|
|
||||||
exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = `
|
exports[`UserTeaserHelper with linkToProfile and popover enabled, on touch screen renders button 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<button />
|
<button>
|
||||||
|
<div>
|
||||||
|
Test Content
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = `
|
exports[`UserTeaserHelper with linkToProfile, on desktop renders link 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<a />
|
<a>
|
||||||
|
<div>
|
||||||
|
Test Content
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`UserTeaserHelper without linkToProfile renders span 1`] = `
|
exports[`UserTeaserHelper without linkToProfile renders span 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<span />
|
<span>
|
||||||
|
<div>
|
||||||
|
Test Content
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user