Merge pull request #7497 from ouchihao/codex/bug-group-modal-cleanup

fix(groups): clean up modal listeners and pending searches
This commit is contained in:
Wesley Liddick
2026-09-28 09:42:09 +08:00
committed by GitHub
3 changed files with 40 additions and 2 deletions
@@ -206,7 +206,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
@@ -420,6 +420,10 @@ const handleClickOutside = () => { showDropdown.value = false }
if (typeof document !== 'undefined') {
document.addEventListener('click', handleClickOutside)
}
onUnmounted(() => {
clearTimeout(searchTimeout)
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
@@ -239,7 +239,7 @@
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { adminAPI } from '@/api/admin'
@@ -490,6 +490,10 @@ const handleClickOutside = () => {
if (typeof document !== 'undefined') {
document.addEventListener('click', handleClickOutside)
}
onUnmounted(() => {
clearTimeout(searchTimeout)
document.removeEventListener('click', handleClickOutside)
})
</script>
<style scoped>
@@ -0,0 +1,30 @@
import { flushPromises, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import GroupRPMOverridesModal from '../GroupRPMOverridesModal.vue'
import GroupRateMultipliersModal from '../GroupRateMultipliersModal.vue'
import type { AdminGroup } from '@/types'
const mocks = vi.hoisted(() => ({ list: vi.fn().mockResolvedValue({ items: [] }), getGroupRPMOverrides: vi.fn().mockResolvedValue([]), getGroupRateMultipliers: vi.fn().mockResolvedValue([]) }))
vi.mock('@/api/admin', () => ({ adminAPI: { users: mocks, groups: mocks } }))
vi.mock('@/stores/app', () => ({ useAppStore: () => ({ showError: vi.fn() }) }))
vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key }) }))
beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers() })
afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers() })
describe.each([GroupRPMOverridesModal, GroupRateMultipliersModal])('group modal lifecycle', (component) => {
function open() {
return mount(component, { props: { show: true, group: { id: 1, name: 'Group', platform: 'openai' } as AdminGroup },
global: { stubs: { BaseDialog: { template: '<div><slot /></div>' }, Icon: true, PlatformIcon: true, Pagination: true } } })
}
it('removes its document click listener on unmount', () => {
const add = vi.spyOn(document, 'addEventListener'); const remove = vi.spyOn(document, 'removeEventListener')
const w = open()
const handlers = add.mock.calls.filter(([event]) => event === 'click').map(([, handler]) => handler)
expect(handlers.length).toBeGreaterThan(0)
w.unmount()
for (const handler of handlers) expect(remove).toHaveBeenCalledWith('click', handler)
})
it('cancels a queued search when navigating away', async () => {
const w = open(); await w.get('input[type="text"]').setValue('alice')
w.unmount(); await vi.advanceTimersByTimeAsync(300); await flushPromises()
expect(mocks.list).not.toHaveBeenCalled()
})
})