mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6798 from savvym/codex/fix-account-menu-overflow
fix(frontend): 修复账号操作菜单在视口底部被截断
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show && position">
|
||||
<div v-if="show && anchorRect">
|
||||
<!-- Backdrop: click anywhere outside to close -->
|
||||
<div class="fixed inset-0 z-[9998]" @click="emit('close')"></div>
|
||||
<div
|
||||
class="action-menu-content fixed z-[9999] w-52 overflow-hidden rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800"
|
||||
:style="{ top: position.top + 'px', left: position.left + 'px' }"
|
||||
ref="menuRef"
|
||||
class="action-menu-content fixed z-[9999] w-52 overflow-y-auto overscroll-contain rounded-xl bg-white shadow-lg ring-1 ring-black/5 dark:bg-dark-800"
|
||||
:style="menuStyle"
|
||||
@click.stop
|
||||
>
|
||||
<div class="py-1">
|
||||
@@ -62,14 +63,48 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onUnmounted } from 'vue'
|
||||
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||
import { useResizeObserver, useWindowSize } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Icon } from '@/components/icons'
|
||||
import type { Account } from '@/types'
|
||||
|
||||
const props = defineProps<{ show: boolean; account: Account | null; position: { top: number; left: number } | null }>()
|
||||
const props = defineProps<{ show: boolean; account: Account | null; anchorRect: DOMRect | null }>()
|
||||
const emit = defineEmits(['close', 'test', 'stats', 'schedule', 'duplicate', 'reauth', 'refresh-token', 'recover-state', 'reset-quota', 'set-privacy', 'create-spark-shadow'])
|
||||
const { t } = useI18n()
|
||||
const menuRef = ref<HTMLElement | null>(null)
|
||||
const { width: viewportWidth, height: viewportHeight } = useWindowSize()
|
||||
const viewportPadding = 8
|
||||
const menuPosition = ref({ top: viewportPadding, left: viewportPadding })
|
||||
const menuStyle = computed(() => ({
|
||||
top: `${menuPosition.value.top}px`,
|
||||
left: `${menuPosition.value.left}px`,
|
||||
maxWidth: `${Math.max(0, viewportWidth.value - viewportPadding * 2)}px`,
|
||||
maxHeight: `${Math.max(0, viewportHeight.value - viewportPadding * 2)}px`
|
||||
}))
|
||||
|
||||
const updatePosition = () => {
|
||||
if (!menuRef.value || !props.anchorRect) return
|
||||
|
||||
const { width, height } = menuRef.value.getBoundingClientRect()
|
||||
const anchor = props.anchorRect
|
||||
const gap = 4
|
||||
const maxTop = viewportHeight.value - height - viewportPadding
|
||||
const top = anchor.bottom + gap <= maxTop
|
||||
? anchor.bottom + gap
|
||||
: anchor.top - height - gap
|
||||
const left = viewportWidth.value < 768
|
||||
? anchor.left + anchor.width / 2 - width / 2
|
||||
: anchor.right - width
|
||||
|
||||
menuPosition.value.top = Math.max(viewportPadding, Math.min(top, maxTop))
|
||||
menuPosition.value.left = Math.max(viewportPadding, Math.min(left, viewportWidth.value - width - viewportPadding))
|
||||
}
|
||||
|
||||
// Measure after rendering; menu items and translated labels can change its size.
|
||||
watch([menuRef, () => props.anchorRect, viewportWidth, viewportHeight], updatePosition, { flush: 'post' })
|
||||
useResizeObserver(menuRef, updatePosition)
|
||||
|
||||
const canDuplicate = computed(() => {
|
||||
if (!props.account || props.account.parent_account_id != null) return false
|
||||
return ['apikey', 'upstream', 'bedrock', 'service_account'].includes(props.account.type)
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import AccountActionMenu from '../AccountActionMenu.vue'
|
||||
import type { Account } from '@/types'
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({ t: (key: string) => key })
|
||||
}))
|
||||
|
||||
const account = {
|
||||
id: 1,
|
||||
platform: 'openai',
|
||||
type: 'oauth',
|
||||
status: 'active',
|
||||
rate_limit_reset_at: '2099-01-01T00:00:00Z'
|
||||
} as Account
|
||||
|
||||
let menuHeight: number
|
||||
let resizeMenu: ResizeObserverCallback
|
||||
|
||||
const getMenu = () => document.body.querySelector<HTMLElement>('.action-menu-content')!
|
||||
const getTop = () => Number.parseFloat(getMenu().style.top)
|
||||
const getLeft = () => Number.parseFloat(getMenu().style.left)
|
||||
|
||||
function setViewport(width: number, height: number) {
|
||||
vi.stubGlobal('innerWidth', width)
|
||||
vi.stubGlobal('innerHeight', height)
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
}
|
||||
|
||||
async function mountMenu(anchorRect = new DOMRect(950, 720, 32, 24)) {
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, anchorRect },
|
||||
global: { stubs: { Icon: true } }
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
describe('AccountActionMenu viewport positioning', () => {
|
||||
beforeEach(() => {
|
||||
menuHeight = 305
|
||||
setViewport(1024, 768)
|
||||
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
|
||||
if (!this.classList.contains('action-menu-content')) return new DOMRect()
|
||||
return new DOMRect(0, 0, Math.min(208, window.innerWidth - 16), Math.min(menuHeight, window.innerHeight - 16))
|
||||
})
|
||||
vi.stubGlobal('ResizeObserver', class {
|
||||
constructor(callback: ResizeObserverCallback) { resizeMenu = callback }
|
||||
observe = vi.fn()
|
||||
disconnect = vi.fn()
|
||||
unobserve = vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('measures a long OAuth menu and opens above the last row', async () => {
|
||||
await mountMenu()
|
||||
|
||||
expect(getTop()).toBe(720 - 305 - 4)
|
||||
expect(getLeft()).toBe(982 - 208)
|
||||
expect(getTop() + menuHeight).toBeLessThanOrEqual(window.innerHeight - 8)
|
||||
expect(getMenu().textContent).toContain('admin.accounts.recoverState')
|
||||
})
|
||||
|
||||
it('opens below the trigger when the complete menu fits', async () => {
|
||||
await mountMenu(new DOMRect(500, 100, 32, 24))
|
||||
|
||||
expect(getTop()).toBe(128)
|
||||
expect(getLeft()).toBe(324)
|
||||
})
|
||||
|
||||
it('centers a mobile menu on its trigger and flips above the last row', async () => {
|
||||
setViewport(390, 600)
|
||||
await mountMenu(new DOMRect(179, 550, 32, 24))
|
||||
|
||||
expect(getTop()).toBe(241)
|
||||
expect(getLeft()).toBe(91)
|
||||
})
|
||||
|
||||
it.each([0, 370])('keeps mobile menus within the horizontal margins at x=%i', async (x) => {
|
||||
setViewport(390, 600)
|
||||
await mountMenu(new DOMRect(x, 100, 24, 24))
|
||||
|
||||
expect(getLeft()).toBeGreaterThanOrEqual(8)
|
||||
expect(getLeft() + 208).toBeLessThanOrEqual(382)
|
||||
})
|
||||
|
||||
it('constrains an oversized menu and preserves the recover-state action', async () => {
|
||||
setViewport(180, 220)
|
||||
const wrapper = await mountMenu(new DOMRect(150, 180, 24, 24))
|
||||
|
||||
expect(getTop()).toBe(8)
|
||||
expect(getLeft()).toBe(8)
|
||||
expect(getMenu().style.maxHeight).toBe('204px')
|
||||
expect(getMenu().style.maxWidth).toBe('164px')
|
||||
expect(getMenu().classList.contains('overflow-y-auto')).toBe(true)
|
||||
expect(getMenu().classList.contains('overscroll-contain')).toBe(true)
|
||||
|
||||
const recover = Array.from(getMenu().querySelectorAll('button'))
|
||||
.find(button => button.textContent?.includes('admin.accounts.recoverState'))!
|
||||
recover.click()
|
||||
expect(wrapper.emitted('recover-state')).toEqual([[account]])
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('repositions when menu content grows', async () => {
|
||||
await mountMenu()
|
||||
menuHeight = 420
|
||||
resizeMenu([], {} as ResizeObserver)
|
||||
await flushPromises()
|
||||
|
||||
expect(getTop()).toBe(296)
|
||||
})
|
||||
|
||||
it('repositions and updates size limits after a viewport resize', async () => {
|
||||
await mountMenu()
|
||||
setViewport(320, 240)
|
||||
await flushPromises()
|
||||
|
||||
expect(getTop()).toBe(8)
|
||||
expect(getLeft()).toBe(104)
|
||||
expect(getMenu().style.maxHeight).toBe('224px')
|
||||
expect(getMenu().style.maxWidth).toBe('304px')
|
||||
})
|
||||
|
||||
it('remeasures after reopening on another row', async () => {
|
||||
const wrapper = await mountMenu()
|
||||
await wrapper.setProps({ show: false })
|
||||
expect(getMenu()).toBeNull()
|
||||
await wrapper.setProps({ show: true, anchorRect: new DOMRect(400, 100, 32, 24) })
|
||||
await flushPromises()
|
||||
|
||||
expect(getTop()).toBe(128)
|
||||
expect(getLeft()).toBe(224)
|
||||
})
|
||||
|
||||
it('still closes on Escape and backdrop clicks', async () => {
|
||||
const wrapper = await mountMenu()
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
||||
const backdrop = getMenu().previousElementSibling as HTMLElement
|
||||
backdrop.click()
|
||||
|
||||
expect(wrapper.emitted('close')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
+11
-11
@@ -42,7 +42,7 @@ function makeAccount(overrides: Partial<Account>): Account {
|
||||
}
|
||||
}
|
||||
|
||||
const position = { top: 100, left: 100 }
|
||||
const anchorRect = new DOMRect(100, 100, 24, 24)
|
||||
|
||||
// AccountActionMenu uses <Teleport to="body">; content is rendered in document.body, not in wrapper.
|
||||
const getBodyText = () => document.body.textContent ?? ''
|
||||
@@ -52,7 +52,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('普通账号显示「复制账号」按钮', () => {
|
||||
const account = makeAccount({ platform: 'anthropic', type: 'apikey', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).toContain('admin.accounts.duplicateAccount')
|
||||
@@ -62,7 +62,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('影子账号隐藏「复制账号」按钮', () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: 42 })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).not.toContain('admin.accounts.duplicateAccount')
|
||||
@@ -72,7 +72,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it.each(['oauth', 'setup-token'] as const)('%s 账号隐藏「复制账号」按钮,避免共享可轮换令牌', (type) => {
|
||||
const account = makeAccount({ platform: 'openai', type, parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).not.toContain('admin.accounts.duplicateAccount')
|
||||
@@ -82,7 +82,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('点击「复制账号」触发 duplicate 事件并携带 account', async () => {
|
||||
const account = makeAccount({ platform: 'anthropic', type: 'apikey', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('OpenAI OAuth 母账号(无 parent_account_id)显示「创建 spark 影子」按钮', () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).toContain('admin.accounts.createSparkShadow')
|
||||
@@ -111,7 +111,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('影子账号(parent_account_id 非 null)隐藏「创建 spark 影子」按钮', () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: 42 })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).not.toContain('admin.accounts.createSparkShadow')
|
||||
@@ -121,7 +121,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('非 OpenAI 账号隐藏「创建 spark 影子」按钮', () => {
|
||||
const account = makeAccount({ platform: 'antigravity', type: 'oauth', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
expect(getBodyText()).not.toContain('admin.accounts.createSparkShadow')
|
||||
@@ -131,7 +131,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('影子账号隐藏凭据/隐私类操作(重授权/刷新token/隐私)— 外审 G4', () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: 42 })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
const body = getBodyText()
|
||||
@@ -144,7 +144,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('普通 OpenAI OAuth 母账号仍显示凭据/隐私类操作', () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
const body = getBodyText()
|
||||
@@ -156,7 +156,7 @@ describe('AccountActionMenu — spark shadow 按钮可见性', () => {
|
||||
it('点击按钮触发 create-spark-shadow 事件并携带 account', async () => {
|
||||
const account = makeAccount({ platform: 'openai', type: 'oauth', parent_account_id: null })
|
||||
const wrapper = mount(AccountActionMenu, {
|
||||
props: { show: true, account, position },
|
||||
props: { show: true, account, anchorRect },
|
||||
attachTo: document.body,
|
||||
})
|
||||
|
||||
|
||||
@@ -456,7 +456,7 @@
|
||||
<AccountTestModal :show="showTest" :account="testingAcc" @close="closeTestModal" />
|
||||
<AccountStatsModal :show="showStats" :account="statsAcc" @close="closeStatsModal" />
|
||||
<ScheduledTestsPanel :show="showSchedulePanel" :account-id="scheduleAcc?.id ?? null" :model-options="scheduleModelOptions" @close="closeSchedulePanel" />
|
||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :position="menu.pos" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @duplicate="handleDuplicateAccount" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" @create-spark-shadow="handleCreateSparkShadow" />
|
||||
<AccountActionMenu :show="menu.show" :account="menu.acc" :anchor-rect="menu.anchorRect" @close="menu.show = false" @test="handleTest" @stats="handleViewStats" @schedule="handleSchedule" @duplicate="handleDuplicateAccount" @reauth="handleReAuth" @refresh-token="handleRefresh" @recover-state="handleRecoverState" @reset-quota="handleResetQuota" @set-privacy="handleSetPrivacy" @create-spark-shadow="handleCreateSparkShadow" />
|
||||
<SyncFromCrsModal :show="showSync" @close="showSync = false" @synced="reload" />
|
||||
<ImportDataModal :show="showImportData" @close="showImportData = false" @imported="handleDataImported" />
|
||||
<BulkEditAccountModal
|
||||
@@ -614,7 +614,7 @@ const showSchedulePanel = ref(false)
|
||||
const scheduleAcc = ref<Account | null>(null)
|
||||
const scheduleModelOptions = ref<SelectOption[]>([])
|
||||
const togglingSchedulable = ref<number | null>(null)
|
||||
const menu = reactive<{show:boolean, acc:Account|null, pos:{top:number, left:number}|null}>({ show: false, acc: null, pos: null })
|
||||
const menu = reactive<{show:boolean, acc:Account|null, anchorRect:DOMRect|null}>({ show: false, acc: null, anchorRect: null })
|
||||
const exportingData = ref(false)
|
||||
const probingUpstreamBilling = reactive(new Set<number>())
|
||||
const upstreamBillingProbeGloballyEnabled = ref<boolean | undefined>(undefined)
|
||||
@@ -1844,53 +1844,8 @@ const handleEdit = async (a: AccountListItem) => {
|
||||
}
|
||||
const openMenu = (a: Account, e: MouseEvent) => {
|
||||
menu.acc = a
|
||||
|
||||
const target = e.currentTarget as HTMLElement
|
||||
if (target) {
|
||||
const rect = target.getBoundingClientRect()
|
||||
const menuWidth = 200
|
||||
const menuHeight = 240
|
||||
const padding = 8
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
|
||||
let left: number
|
||||
let top: number
|
||||
|
||||
if (viewportWidth < 768) {
|
||||
// 居中显示,水平位置
|
||||
left = Math.max(padding, Math.min(
|
||||
rect.left + rect.width / 2 - menuWidth / 2,
|
||||
viewportWidth - menuWidth - padding
|
||||
))
|
||||
|
||||
// 优先显示在按钮下方
|
||||
top = rect.bottom + 4
|
||||
|
||||
// 如果下方空间不够,显示在上方
|
||||
if (top + menuHeight > viewportHeight - padding) {
|
||||
top = rect.top - menuHeight - 4
|
||||
// 如果上方也不够,就贴在视口顶部
|
||||
if (top < padding) {
|
||||
top = padding
|
||||
}
|
||||
}
|
||||
} else {
|
||||
left = Math.max(padding, Math.min(
|
||||
e.clientX - menuWidth,
|
||||
viewportWidth - menuWidth - padding
|
||||
))
|
||||
top = e.clientY
|
||||
if (top + menuHeight > viewportHeight - padding) {
|
||||
top = viewportHeight - menuHeight - padding
|
||||
}
|
||||
}
|
||||
|
||||
menu.pos = { top, left }
|
||||
} else {
|
||||
menu.pos = { top: e.clientY, left: e.clientX - 200 }
|
||||
}
|
||||
|
||||
menu.anchorRect = target.getBoundingClientRect()
|
||||
menu.show = true
|
||||
}
|
||||
const toggleSelectAllVisible = (event: Event) => {
|
||||
@@ -2538,7 +2493,8 @@ const proxyExpiryText = (p: AccountProxy): string => {
|
||||
}
|
||||
|
||||
// 表格滚动时关闭行操作菜单,并让顶部工具菜单继续贴紧触发按钮。
|
||||
const handleScroll = () => {
|
||||
const handleScroll = (event: Event) => {
|
||||
if (event.target instanceof Element && event.target.closest('.action-menu-content')) return
|
||||
menu.show = false
|
||||
if (showAccountToolsDropdown.value) updateAccountToolsDropdownPosition()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { DOMWrapper, flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import AccountsView from '../AccountsView.vue'
|
||||
@@ -88,8 +88,9 @@ const AccountStatsModalStub = defineComponent({
|
||||
template: '<div data-test="stats-account">{{ show ? account?.name : "" }}</div>'
|
||||
})
|
||||
|
||||
function mountView() {
|
||||
function mountView(stubActionMenu = true) {
|
||||
return mount(AccountsView, {
|
||||
attachTo: document.body,
|
||||
global: {
|
||||
stubs: {
|
||||
AppLayout: { template: '<div><slot /></div>' },
|
||||
@@ -100,7 +101,7 @@ function mountView() {
|
||||
AccountBulkActionsBar: true,
|
||||
Pagination: true,
|
||||
ConfirmDialog: true,
|
||||
AccountActionMenu: true,
|
||||
AccountActionMenu: stubActionMenu,
|
||||
ImportDataModal: true,
|
||||
ReAuthAccountModal: true,
|
||||
AccountTestModal: AccountTestModalStub,
|
||||
@@ -122,7 +123,7 @@ function mountView() {
|
||||
UpstreamBillingRateCell: true,
|
||||
HelpTooltip: true,
|
||||
Icon: true,
|
||||
Teleport: true
|
||||
Teleport: stubActionMenu
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -189,6 +190,27 @@ describe('admin AccountsView lite account list', () => {
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps the action menu open during internal scrolling but closes it on table scrolling', async () => {
|
||||
const wrapper = mountView(false)
|
||||
await flushPromises()
|
||||
|
||||
const trigger = wrapper.findAll('button').find(button => button.text() === 'common.more')!
|
||||
await trigger.trigger('click')
|
||||
const menu = new DOMWrapper(document.body.querySelector('.action-menu-content')!)
|
||||
menu.element.dispatchEvent(new Event('scroll'))
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(AccountActionMenu).props('show')).toBe(true)
|
||||
|
||||
menu.get('button').element.dispatchEvent(new Event('scroll'))
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(AccountActionMenu).props('show')).toBe(true)
|
||||
|
||||
wrapper.getComponent(DataTableStub).element.dispatchEvent(new Event('scroll'))
|
||||
await flushPromises()
|
||||
expect(wrapper.findComponent(AccountActionMenu).props('show')).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps lite=1 on automatic ETag refreshes', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.spyOn(document, 'hidden', 'get').mockReturnValue(false)
|
||||
|
||||
Reference in New Issue
Block a user