chore: improve playback and related services (#1570)

This commit is contained in:
Phan An 2022-10-31 00:13:57 +01:00 committed by GitHub
parent d7a0b69706
commit bd6617dc17
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 256 additions and 158 deletions

View file

@ -26,9 +26,9 @@
<script lang="ts" setup>
import { defineAsyncComponent, nextTick, onMounted, provide, ref, watch } from 'vue'
import { eventBus, hideOverlay, requireInjection, showOverlay } from '@/utils'
import { hideOverlay, requireInjection, showOverlay } from '@/utils'
import { commonStore, preferenceStore as preferences, queueStore } from '@/stores'
import { authService, playbackService, socketListener, socketService, uploadService } from '@/services'
import { authService, socketListener, socketService, uploadService } from '@/services'
import { CurrentSongKey, DialogBoxKey, MessageToasterKey, RouterKey } from '@/symbols'
import { useNetworkStatus } from '@/composables'
@ -97,7 +97,6 @@ const init = async () => {
await commonStore.init()
await nextTick()
playbackService.init()
await requestNotificationPermission()
window.addEventListener('beforeunload', (e: BeforeUnloadEvent) => {
@ -110,9 +109,6 @@ const init = async () => {
await socketService.init() && socketListener.listen()
hideOverlay()
// Let all other components know we're ready.
eventBus.emit('KOEL_READY')
} catch (err) {
authenticated.value = false
throw err
@ -125,7 +121,7 @@ const onDragOver = (e: DragEvent) => {
showDropZone.value = Boolean(e.dataTransfer?.types.includes('Files')) && router.$currentRoute.value.screen !== 'Upload'
}
watch(() => queueStore.current, song => (currentSong.value = song))
watch(() => queueStore.current, song => song && (currentSong.value = song))
const onDragEnd = () => (showDropZone.value = false)
const onDrop = () => (showDropZone.value = false)

View file

@ -7,7 +7,9 @@
</div>
</template>
<script lang="ts" setup></script>
<script lang="ts" setup>
//
</script>
<style lang="scss">
// can't be scoped as it would be overridden by the plyr css

View file

@ -0,0 +1,26 @@
import { waitFor } from '@testing-library/vue'
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { playbackService, volumeManager } from '@/services'
import { eventBus } from '@/utils'
import { preferenceStore } from '@/stores'
import Component from './index.vue'
new class extends UnitTestCase {
protected test () {
it('initializes playback services', async () => {
const initPlaybackMock = this.mock(playbackService, 'init')
const initVolumeMock = this.mock(volumeManager, 'init')
const emitMock = this.mock(eventBus, 'emit')
this.render(Component)
preferenceStore.initialized.value = true
await waitFor(() => {
expect(initPlaybackMock).toHaveBeenCalled()
expect(initVolumeMock).toHaveBeenCalled()
expect(emitMock).toHaveBeenCalledWith('INIT_EQUALIZER')
})
})
}
}

View file

@ -11,9 +11,11 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { eventBus, requireInjection } from '@/utils'
import { nextTick, ref, watch } from 'vue'
import { eventBus, isAudioContextSupported, requireInjection } from '@/utils'
import { CurrentSongKey } from '@/symbols'
import { preferenceStore } from '@/stores'
import { audioService, playbackService, volumeManager } from '@/services'
import AudioPlayer from '@/components/layout/app-footer/AudioPlayer.vue'
import SongInfo from '@/components/layout/app-footer/FooterSongInfo.vue'
@ -25,6 +27,28 @@ const song = requireInjection(CurrentSongKey, ref(null))
const requestContextMenu = (event: MouseEvent) => {
song.value && eventBus.emit('SONG_CONTEXT_MENU_REQUESTED', event, song.value)
}
const initPlaybackRelatedServices = async () => {
const plyrWrapper = document.querySelector<HTMLElement>('.plyr')
const volumeInput = document.querySelector<HTMLInputElement>('#volumeInput')
if (!plyrWrapper || !volumeInput) {
await nextTick()
await initPlaybackRelatedServices()
return
}
playbackService.init(plyrWrapper)
volumeManager.init(volumeInput)
isAudioContextSupported && audioService.init(playbackService.player.media)
eventBus.emit('INIT_EQUALIZER')
}
watch(preferenceStore.initialized, async initialized => {
if (!initialized) return
await initPlaybackRelatedServices()
}, { immediate: true })
</script>
<style lang="scss" scoped>

View file

@ -1,13 +1,12 @@
import { expect, it, vi } from 'vitest'
import { fireEvent } from '@testing-library/vue'
import { eventBus } from '@/utils'
import { preferenceStore } from '@/stores'
import UnitTestCase from '@/__tests__/UnitTestCase'
import SupportKoel from './SupportKoel.vue'
new class extends UnitTestCase {
protected beforeEach () {
super.beforeEach(() => vi.useFakeTimers());
super.beforeEach(() => vi.useFakeTimers())
}
protected afterEach () {
@ -18,13 +17,13 @@ new class extends UnitTestCase {
}
private async renderComponent () {
const result = this.render(SupportKoel)
eventBus.emit('KOEL_READY')
preferenceStore.initialized.value = true
const rendered = this.render(SupportKoel)
vi.advanceTimersByTime(30 * 60 * 1000)
await this.tick()
return result
return rendered
}
protected test () {
@ -34,6 +33,7 @@ new class extends UnitTestCase {
it('does not show if user so demands', async () => {
preferenceStore.state.supportBarNoBugging = true
preferenceStore.initialized.value = true
expect((await this.renderComponent()).queryByTestId('support-bar')).toBeNull()
})

View file

@ -16,31 +16,26 @@
<script lang="ts" setup>
import isMobile from 'ismobilejs'
import { computed, ref, toRef } from 'vue'
import { eventBus } from '@/utils'
import { ref, watch } from 'vue'
import { preferenceStore } from '@/stores'
const delayUntilShow = 30 * 60 * 1000
let timeoutHandle = 0
const delayUntilShow = 30 * 60 * 1000 // 30 minutes
const shown = ref(false)
const noBugging = toRef(preferenceStore.state, 'supportBarNoBugging')
const canNag = computed(() => !isMobile.any && !noBugging.value)
const setUpShowBarTimeout = () => (timeoutHandle = window.setTimeout(() => (shown.value = true), delayUntilShow))
const close = () => {
shown.value = false
window.clearTimeout(timeoutHandle)
}
const setUpShowBarTimeout = () => setTimeout(() => (shown.value = true), delayUntilShow)
const close = () => shown.value = false
const stopBugging = () => {
preferenceStore.set('supportBarNoBugging', true)
close()
}
eventBus.on('KOEL_READY', () => canNag.value && setUpShowBarTimeout())
watch(preferenceStore.initialized, initialized => {
if (!initialized) return
if (preferenceStore.state.supportBarNoBugging || isMobile.any) return
setUpShowBarTimeout()
}, { immediate: true })
</script>
<style lang="scss" scoped>

View file

@ -1,32 +1,42 @@
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { fireEvent } from '@testing-library/vue'
import { playbackService, socketService } from '@/services'
import { socketService, volumeManager } from '@/services'
import { preferenceStore } from '@/stores'
import Volume from './Volume.vue'
new class extends UnitTestCase {
protected beforeEach (cb?: Closure) {
super.beforeEach(() => {
preferenceStore.state.volume = 5
volumeManager.init(document.createElement('input'))
})
}
protected test () {
it('mutes and unmutes', async () => {
const muteMock = this.mock(playbackService, 'mute')
const unmuteMock = this.mock(playbackService, 'unmute')
const { getByRole } = this.render(Volume)
const { getByTitle, html } = this.render(Volume)
expect(html()).toMatchSnapshot()
expect(volumeManager.volume.value).toEqual(5)
await fireEvent.click(getByRole('button'))
expect(muteMock).toHaveBeenCalledOnce()
await fireEvent.click(getByTitle('Mute'))
expect(html()).toMatchSnapshot()
expect(volumeManager.volume.value).toEqual(0)
await fireEvent.click(getByRole('button'))
expect(unmuteMock).toHaveBeenCalledOnce()
await fireEvent.click(getByTitle('Unmute'))
expect(html()).toMatchSnapshot()
expect(volumeManager.volume.value).toEqual(5)
})
it('sets and broadcasts volume', async () => {
const setVolumeMock = this.mock(playbackService, 'setVolume')
const setMock = this.mock(volumeManager, 'set')
const broadCastMock = this.mock(socketService, 'broadcast')
const { getByRole } = this.render(Volume)
await fireEvent.update(getByRole('slider'), '4.2')
await fireEvent.change(getByRole('slider'))
expect(setVolumeMock).toHaveBeenCalledWith(4.2)
expect(setMock).toHaveBeenCalledWith(4.2)
expect(broadCastMock).toHaveBeenCalledWith('SOCKET_VOLUME_CHANGED', 4.2)
})
}

View file

@ -38,30 +38,20 @@
<script lang="ts" setup>
import { faVolumeHigh, faVolumeLow, faVolumeMute } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import { playbackService, socketService } from '@/services'
import { preferenceStore as preferences } from '@/stores'
import { eventBus } from '@/utils'
import { computed } from 'vue'
import { socketService, volumeManager } from '@/services'
const level = ref<'muted' | 'discreet' | 'loud'>()
const volume = volumeManager.volume
const mute = () => {
playbackService.mute()
level.value = 'muted'
}
const level = computed(() => {
if (volume.value === 0) return 'muted'
if (volume.value < 3) return 'discreet'
return 'loud'
})
const unmute = () => {
playbackService.unmute()
level.value = preferences.volume < 3 ? 'discreet' : 'loud'
}
const setVolume = (e: InputEvent) => {
const volume = parseFloat((e.target as HTMLInputElement).value)
playbackService.setVolume(volume)
setLevel(volume)
}
const setLevel = (volume: number) => (level.value = volume === 0 ? 'muted' : volume < 3 ? 'discreet' : 'loud')
const mute = () => volumeManager.mute()
const unmute = () => volumeManager.unmute()
const setVolume = (e: InputEvent) => volumeManager.set(parseFloat((e.target as HTMLInputElement).value))
/**
* Broadcast the volume changed event to remote controller.
@ -69,8 +59,6 @@ const setLevel = (volume: number) => (level.value = volume === 0 ? 'muted' : vol
const broadcastVolume = (e: InputEvent) => {
socketService.broadcast('SOCKET_VOLUME_CHANGED', parseFloat((e.target as HTMLInputElement).value))
}
eventBus.on('KOEL_READY', () => setLevel(preferences.volume))
</script>
<style lang="scss">

View file

@ -0,0 +1,7 @@
// Vitest Snapshot v1
exports[`mutes and unmutes 1`] = `<span id="volume" class="volume loud"><span role="button" tabindex="0" title="Unmute" style="display: none;"><br data-testid="icon" icon="[object Object]" fixed-width=""></span><span role="button" tabindex="0" title="Mute"><br data-testid="icon" icon="[object Object]" fixed-width=""></span><input id="volumeInput" class="plyr__volume" max="10" role="slider" step="0.1" title="Volume" type="range"></span>`;
exports[`mutes and unmutes 2`] = `<span id="volume" class="volume muted"><span role="button" tabindex="0" title="Unmute" style=""><br data-testid="icon" icon="[object Object]" fixed-width=""></span><span role="button" tabindex="0" title="Mute" style="display: none;"><br data-testid="icon" icon="[object Object]" fixed-width=""></span><input id="volumeInput" class="plyr__volume" max="10" role="slider" step="0.1" title="Volume" type="range"></span>`;
exports[`mutes and unmutes 3`] = `<span id="volume" class="volume loud"><span role="button" tabindex="0" title="Unmute" style="display: none;"><br data-testid="icon" icon="[object Object]" fixed-width=""></span><span role="button" tabindex="0" title="Mute" style=""><br data-testid="icon" icon="[object Object]" fixed-width=""></span><input id="volumeInput" class="plyr__volume" max="10" role="slider" step="0.1" title="Volume" type="range"></span>`;

View file

@ -1,5 +1,4 @@
export type EventName =
'KOEL_READY'
| 'LOG_OUT'
| 'TOGGLE_SIDEBAR'
| 'SHOW_OVERLAY'

View file

@ -1,18 +1,15 @@
export const audioService = {
unlocked: false,
context: null as unknown as AudioContext,
source: null as unknown as MediaElementAudioSourceNode,
element: null as unknown as HTMLMediaElement,
init (element: HTMLMediaElement) {
const AudioContext = window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext
this.context = new AudioContext()
this.source = this.context.createMediaElementSource(element)
this.element = element
this.unlockAudioContext()
},
getContext () {
@ -25,5 +22,26 @@ export const audioService = {
getElement () {
return this.element
},
/**
* Attempt to unlock the audio context on mobile devices by creating and playing a silent buffer upon the
* first user interaction.
*/
unlockAudioContext () {
['touchend', 'touchstart', 'click'].forEach(event => {
document.addEventListener(event, () => {
if (this.unlocked) return
const source = this.context.createBufferSource()
source.buffer = this.context.createBuffer(1, 1, 22050)
source.connect(this.context.destination)
source.start(0)
this.unlocked = true
}, {
once: true
})
})
}
}

View file

@ -10,3 +10,4 @@ export * from './authService'
export * from './mediaInfoService'
export * from './cache'
export * from './socketListener'
export * from './volumeManager'

View file

@ -23,14 +23,6 @@ new class extends UnitTestCase {
<div class="plyr">
<audio crossorigin="anonymous" controls/>
</div>
<input
class="plyr__volume"
id="volumeRange"
max="10"
step="0.1"
title="Volume"
type="range"
>
`
window.AudioContext = vi.fn().mockImplementation(() => ({
@ -55,10 +47,10 @@ new class extends UnitTestCase {
it('only initializes once', () => {
const spy = vi.spyOn(plyr, 'setup')
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
expect(spy).toHaveBeenCalled()
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
expect(spy).toHaveBeenCalledTimes(1)
})
@ -76,7 +68,7 @@ new class extends UnitTestCase {
}))
this.setReadOnlyProperty(playbackService, 'isTranscoding', isTranscoding)
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
const mediaElement = playbackService.player.media
@ -91,7 +83,7 @@ new class extends UnitTestCase {
})
it('plays next song if current song is errored', () => {
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
const playNextMock = this.mock(playbackService, 'playNext')
playbackService.player!.media.dispatchEvent(new Event('error'))
expect(playNextMock).toHaveBeenCalled()
@ -105,7 +97,7 @@ new class extends UnitTestCase {
}
}))
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
const scrobbleMock = this.mock(songStore, 'scrobble')
playbackService.player!.media.dispatchEvent(new Event('ended'))
expect(scrobbleMock).toHaveBeenCalled()
@ -116,7 +108,7 @@ new class extends UnitTestCase {
(repeatMode, restartCalls, playNextCalls) => {
commonStore.state.use_last_fm = false // so that no scrobbling is made unnecessarily
preferences.repeatMode = repeatMode
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
const restartMock = this.mock(playbackService, 'restart')
const playNextMock = this.mock(playbackService, 'playNext')
@ -138,7 +130,7 @@ new class extends UnitTestCase {
this.setReadOnlyProperty(queueStore, 'next', factory<Song>('song', { preloaded }))
this.setReadOnlyProperty(playbackService, 'isTranscoding', isTranscoding)
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
const mediaElement = playbackService.player!.media
@ -297,7 +289,7 @@ new class extends UnitTestCase {
const playMock = this.mock(window.HTMLMediaElement.prototype, 'play')
const broadcastMock = this.mock(socketService, 'broadcast')
playbackService.init()
playbackService.init(document.querySelector('.plyr')!)
await playbackService.resume()
expect(queueStore.current?.playback_state).toEqual('Playing')

View file

@ -1,5 +1,6 @@
import isMobile from 'ismobilejs'
import plyr from 'plyr'
import { watch } from 'vue'
import { shuffle, throttle } from 'lodash'
import {
@ -11,49 +12,30 @@ import {
userStore
} from '@/stores'
import { arrayify, eventBus, isAudioContextSupported, logger } from '@/utils'
import { audioService, socketService } from '@/services'
import { arrayify, isAudioContextSupported, logger } from '@/utils'
import { audioService, socketService, volumeManager } from '@/services'
/**
* The number of seconds before the current song ends to start preload the next one.
*/
const PRELOAD_BUFFER = 30
const DEFAULT_VOLUME_VALUE = 7
const VOLUME_INPUT_SELECTOR = '#volumeInput'
class PlaybackService {
// @ts-ignore
public player: Plyr
// @ts-ignore
private volumeInput: HTMLInputElement
public player!: Plyr
private repeatModes: RepeatMode[] = ['NO_REPEAT', 'REPEAT_ALL', 'REPEAT_ONE']
private initialized = false
public init () {
if (this.initialized) {
return
}
public init (plyrWrapper: HTMLElement) {
if (this.initialized) return
this.player = plyr.setup(plyrWrapper, { controls: [] })[0]
this.listenToMediaEvents(this.player.media)
this.setMediaSessionActionHandlers()
watch(volumeManager.volume, volume => this.player.setVolume(volume), { immediate: true })
this.initialized = true
this.player = plyr.setup('.plyr', {
controls: []
})[0]
this.volumeInput = document.querySelector<HTMLInputElement>(VOLUME_INPUT_SELECTOR)!
this.listenToMediaEvents(this.player.media)
if (isAudioContextSupported) {
try {
this.setVolume(preferences.volume)
} catch (e) {
}
audioService.init(this.player.media)
eventBus.emit('INIT_EQUALIZER')
}
this.setMediaSessionActionHandlers()
}
public registerPlay (song: Song) {
@ -245,29 +227,6 @@ class PlaybackService {
}
}
public getVolume () {
return preferences.volume
}
/**
* @param {Number} volume 0-10
* @param {Boolean=true} persist Whether the volume should be saved into local storage
*/
public setVolume (volume: number, persist = true) {
this.player.setVolume(volume)
persist && (preferences.volume = volume)
this.volumeInput.value = String(volume)
}
public mute () {
this.setVolume(0, false)
}
public unmute () {
preferences.volume = preferences.volume || DEFAULT_VOLUME_VALUE
this.setVolume(preferences.volume)
}
public async stop () {
document.title = 'Koel'
this.player.pause()
@ -344,10 +303,10 @@ class PlaybackService {
navigator.mediaSession.setActionHandler('nexttrack', () => this.playNext())
}
private listenToMediaEvents (mediaElement: HTMLMediaElement) {
mediaElement.addEventListener('error', () => this.playNext(), true)
private listenToMediaEvents (media: HTMLMediaElement) {
media.addEventListener('error', () => this.playNext(), true)
mediaElement.addEventListener('ended', () => {
media.addEventListener('ended', () => {
if (commonStore.state.use_last_fm && userStore.current.preferences!.lastfm_session_key) {
songStore.scrobble(queueStore.current!)
}
@ -363,7 +322,7 @@ class PlaybackService {
if (!currentSong.play_count_registered && !this.isTranscoding) {
// if we've passed 25% of the song, it's safe to say the song has been "played".
// Refer to https://github.com/koel/koel/issues/1087
if (!mediaElement.duration || mediaElement.currentTime * 4 >= mediaElement.duration) {
if (!media.duration || media.currentTime * 4 >= media.duration) {
this.registerPlay(currentSong)
}
}
@ -374,7 +333,7 @@ class PlaybackService {
return
}
if (mediaElement.duration && mediaElement.currentTime + PRELOAD_BUFFER > mediaElement.duration) {
if (media.duration && media.currentTime + PRELOAD_BUFFER > media.duration) {
this.preload(nextSong)
}
}
@ -383,7 +342,7 @@ class PlaybackService {
timeUpdateHandler = throttle(timeUpdateHandler, 1000)
}
mediaElement.addEventListener('timeupdate', timeUpdateHandler)
media.addEventListener('timeupdate', timeUpdateHandler)
}
}

View file

@ -1,4 +1,4 @@
import { playbackService, socketService } from '@/services'
import { playbackService, socketService, volumeManager } from '@/services'
import { favoriteStore, queueStore } from '@/stores'
export const socketListener = {
@ -10,11 +10,11 @@ export const socketListener = {
.listen('SOCKET_GET_STATUS', () => {
socketService.broadcast('SOCKET_STATUS', {
song: queueStore.current,
volume: playbackService.getVolume()
volume: volumeManager.get()
})
})
.listen('SOCKET_GET_CURRENT_SONG', () => socketService.broadcast('SOCKET_SONG', queueStore.current))
.listen('SOCKET_SET_VOLUME', (volume: number) => playbackService.setVolume(volume))
.listen('SOCKET_SET_VOLUME', (volume: number) => volumeManager.set(volume))
.listen('SOCKET_TOGGLE_FAVORITE', () => queueStore.current && favoriteStore.toggleOne(queueStore.current))
}
}

View file

@ -0,0 +1,43 @@
import { expect, it } from 'vitest'
import UnitTestCase from '@/__tests__/UnitTestCase'
import { volumeManager } from '@/services/volumeManager'
import { preferenceStore } from '@/stores'
let input: HTMLInputElement
new class extends UnitTestCase {
protected beforeEach (cb?: Closure) {
super.beforeEach(() => {
preferenceStore.state.volume = 5
input = document.createElement('input')
volumeManager.init(input)
})
}
protected test () {
it('gets volume', () => expect(volumeManager.get()).toEqual(5))
it('sets volume', () => {
volumeManager.set(4.2)
expect(volumeManager.volume.value).toEqual(4.2)
expect(input.value).toEqual('4.2')
expect(preferenceStore.state.volume).toEqual(4.2)
})
it('mutes', () => {
volumeManager.mute()
expect(volumeManager.volume.value).toEqual(0)
expect(input.value).toEqual('0')
// muting should not persist
expect(preferenceStore.state.volume).toEqual(5)
})
it('unmutes', () => {
preferenceStore.state.volume = 7
volumeManager.unmute()
expect(volumeManager.volume.value).toEqual(7)
expect(input.value).toEqual('7')
})
}
}

View file

@ -0,0 +1,35 @@
import { ref } from 'vue'
import { preferenceStore } from '@/stores'
export class VolumeManager {
private input!: HTMLInputElement
public volume = ref(0)
public init (input: HTMLInputElement) {
this.input = input
this.set(preferenceStore.state.volume)
}
public get () {
return this.volume.value
}
public set (volume: number, persist = true) {
if (persist) {
preferenceStore.state.volume = volume
}
this.volume.value = volume
this.input.value = String(volume)
}
public mute () {
this.set(0, false)
}
public unmute () {
this.set(preferenceStore.state.volume)
}
}
export const volumeManager = new VolumeManager()

View file

@ -1,4 +1,4 @@
import { reactive } from 'vue'
import { reactive, ref } from 'vue'
import { userStore } from '@/stores'
import { localStorageService } from '@/services'
@ -20,6 +20,7 @@ interface Preferences extends Record<string, any> {
const preferenceStore = {
storeKey: '',
initialized: ref(false),
state: reactive<Preferences>({
volume: 7,
@ -45,6 +46,8 @@ const preferenceStore = {
this.storeKey = `preferences_${initUser.id}`
Object.assign(this.state, localStorageService.get(this.storeKey, this.state))
this.setupProxy()
this.initialized.value = true
},
/**

View file

@ -10,18 +10,18 @@ new class extends UnitTestCase {
protected test () {
it('listens on a single event', () => {
const mock = vi.fn()
eventBus.on('KOEL_READY', mock)
eventBus.on('SHOW_OVERLAY', mock)
eventBus.emit('KOEL_READY')
eventBus.emit('SHOW_OVERLAY')
expect(mock).toHaveBeenCalledOnce()
})
it('listens with parameters', () => {
const mock = vi.fn()
eventBus.on('KOEL_READY', mock)
eventBus.on('SHOW_OVERLAY', mock)
eventBus.emit('KOEL_READY', 'foo', 'bar')
eventBus.emit('SHOW_OVERLAY', 'foo', 'bar')
expect(mock).toHaveBeenNthCalledWith(1, 'foo', 'bar')
})
@ -31,11 +31,11 @@ new class extends UnitTestCase {
const mock2 = vi.fn()
eventBus.on({
KOEL_READY: mock1,
SHOW_OVERLAY: mock1,
MODAL_SHOW_ABOUT_KOEL: mock2
})
eventBus.emit('KOEL_READY')
eventBus.emit('SHOW_OVERLAY')
expect(mock1).toHaveBeenCalledOnce()
eventBus.emit('MODAL_SHOW_ABOUT_KOEL')
@ -45,10 +45,10 @@ new class extends UnitTestCase {
it('queue up listeners on same event', () => {
const mock1 = vi.fn()
const mock2 = vi.fn()
eventBus.on('KOEL_READY', mock1)
eventBus.on('KOEL_READY', mock2)
eventBus.on('SHOW_OVERLAY', mock1)
eventBus.on('SHOW_OVERLAY', mock2)
eventBus.emit('KOEL_READY')
eventBus.emit('SHOW_OVERLAY')
expect(mock1).toHaveBeenCalledOnce()
expect(mock2).toHaveBeenCalledOnce()