// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from 'vitest' import { flushPromises, mount } from '@vue/test-utils' import { defineComponent } from 'vue' import { createPinia, setActivePinia } from 'pinia' import ArcoVue from '@arco-design/web-vue' import type { FileEntry } from '@/api/hermes/files' const mockMessage = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), })) const mockDialog = vi.hoisted(() => ({ warning: vi.fn(), })) const downloadFileMock = vi.hoisted(() => vi.fn()) const copyToClipboardMock = vi.hoisted(() => vi.fn()) vi.mock('vue-i18n', () => ({ useI18n: () => ({ t: (key: string) => key, }), })) vi.mock('@/api/hermes/download', () => ({ downloadFile: downloadFileMock, })) vi.mock('@/utils/clipboard', () => ({ copyToClipboard: copyToClipboardMock, })) vi.mock('@/utils/file-path', () => ({ getClipboardPathForEntry: (entry: FileEntry) => entry.path, })) // Arco's imperative Message/Dialog APIs need a live DOM, so mock the adapters. vi.mock('@/composables/useMessage', () => ({ useMessage: () => mockMessage })) vi.mock('@/composables/useDialog', () => ({ useDialog: () => mockDialog })) /** * Arco's a-trigger teleports its popup to body in jsdom and a-doption is a * popup-scoped component, so both are stubbed under Arco's *internal* component * names — stubs key off those, not the `a-*` template tags, because Arco is * registered globally via `app.use(ArcoVue)`. * * The stub renders the #content slot inline and lets clicks bubble from the * option element, so a click behaves like the real Doption -> handleSelect path. */ const ATriggerStub = defineComponent({ name: 'Trigger', props: ['popupVisible'], template: '
', }) const ADoptionStub = defineComponent({ name: 'Doption', emits: ['click'], template: '
', }) const arcoGlobal = { plugins: [ArcoVue], stubs: { Trigger: ATriggerStub, Doption: ADoptionStub }, } /** Menu item keys, read from the rendered popup rather than a stub attribute. */ function menuKeys(wrapper: ReturnType): string[] { return wrapper.findAll('.file-context-menu > *').map(node => node.text().trim()) } import FileContextMenu from '@/components/hermes/files/FileContextMenu.vue' import { useFilesStore } from '@/stores/hermes/files' describe('FileContextMenu', () => { beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() }) async function showMenu(wrapper: ReturnType, entry: FileEntry) { ;(wrapper.vm as any).show(new MouseEvent('contextmenu', { clientX: 12, clientY: 34 }), entry) await flushPromises() } function menuItem(wrapper: ReturnType, label: string) { const item = wrapper.findAll('.file-context-menu > *').find(node => node.text().trim() === label) if (!item) throw new Error(`menu item not found: ${label}`) return item } it('offers both edit and preview for previewable text files', async () => { const wrapper = mount(FileContextMenu, { global: arcoGlobal }) const entry: FileEntry = { name: 'Dockerfile', path: 'Dockerfile', isDir: false, size: 42, modTime: '2026-06-02T00:00:00.000Z', } await showMenu(wrapper, entry) const labels = menuKeys(wrapper) expect(labels).toContain('files.edit') expect(labels).toContain('files.preview') expect(labels).toContain('files.download') }) it('invokes the store preview action from the preview menu item', async () => { const store = useFilesStore() const openPreviewSpy = vi.spyOn(store, 'openPreview').mockResolvedValue(undefined) const wrapper = mount(FileContextMenu, { global: arcoGlobal }) const entry: FileEntry = { name: 'README', path: 'README', isDir: false, size: 12, modTime: '2026-06-02T00:00:00.000Z', } await showMenu(wrapper, entry) await menuItem(wrapper, 'files.preview').trigger('click') await flushPromises() expect(openPreviewSpy).toHaveBeenCalledWith(entry) expect(mockMessage.error).not.toHaveBeenCalled() }) it('does not offer preview for non-previewable binary files', async () => { const wrapper = mount(FileContextMenu, { global: arcoGlobal }) const entry: FileEntry = { name: 'archive.zip', path: 'archive.zip', isDir: false, size: 128, modTime: '2026-06-02T00:00:00.000Z', } await showMenu(wrapper, entry) const labels = menuKeys(wrapper) expect(labels).not.toContain('files.preview') expect(labels).not.toContain('files.edit') expect(labels).toContain('files.download') }) })