import { mount } from '@vue/test-utils' import { computed, h, nextTick } from 'vue' import { createPinia } from 'pinia' import { describe, expect, it, vi } from 'vitest' import AppLayout from '../../components/AppLayout.vue' import AppSidebar from '../../components/AppSidebar.vue' import AppTopBar from '../../components/AppTopBar.vue' import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue' import WorkspaceControlsInternal from '../../components/internal/WorkspaceControlsInternal.vue' import WorkspaceShellInternal from '../../components/internal/WorkspaceShellInternal.vue' import WorkspaceSidebarInternal from '../../components/internal/WorkspaceSidebarInternal.vue' import type { PillNavItem, SidebarToolSection } from '../../types' vi.mock('../../composables/usePlatformContext', () => ({ usePlatformContext: vi.fn(() => ({ isIntegrated: computed(() => false), context: { value: { isIntegrated: false, theme: 'system' } }, plugin: computed(() => undefined), user: computed(() => undefined), theme: computed(() => 'system' as const), features: computed(() => undefined), navigate: vi.fn(), notify: vi.fn(), sendToPlatform: vi.fn(), })), })) vi.mock('../../composables/useTheme', () => ({ useTheme: vi.fn(() => ({ isDark: computed(() => false), theme: computed(() => 'light'), toggleTheme: vi.fn(), })), })) const pillNav: PillNavItem[] = [ { id: 'data', label: 'Data' }, { id: 'results', label: 'Results' }, ] const panels: Record = { data: [ { id: 'import', label: 'Import', defaultOpen: true }, ], results: [ { id: 'display', label: 'Display', defaultOpen: true }, ], } interface WorkspaceSlotProps { activeView: string setActiveView: (viewId: string) => void } const globalOptions = { plugins: [createPinia()], stubs: { 'router-link': { props: ['to', 'custom'], template: ` `, }, BaseModal: { template: '
', }, }, } describe('PluginWorkspaceView internal split', () => { it('renders through WorkspaceShellInternal, WorkspaceSidebarInternal and WorkspaceControlsInternal', () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'MS Uploader', subtitle: 'Import and visualize', pillNav, panels, sidebarTitle: 'Tools', sidebarSubtitle: 'Current file', }, slots: { actions: () => h('button', { 'data-test': 'topbar-action' }, 'Run'), 'section-import': () => h('button', { 'data-test': 'import-action' }, 'Upload'), default: ({ activeView }: WorkspaceSlotProps) => h('main', { 'data-test': 'content' }, activeView), }, global: globalOptions, }) // The public adapter renders its three internal pieces... expect(wrapper.findComponent(WorkspaceShellInternal).exists()).toBe(true) expect(wrapper.findComponent(WorkspaceSidebarInternal).exists()).toBe(true) expect(wrapper.findComponent(WorkspaceControlsInternal).exists()).toBe(true) // ...each of which wraps the same underlying SDK components with the same DOM output // the pre-split component produced. expect(wrapper.findComponent(AppLayout).props()).toMatchObject({ floating: true, responsiveSidebar: true, sidebarPosition: 'left', }) expect(wrapper.findComponent(AppTopBar).props()).toMatchObject({ title: 'MS Uploader', subtitle: 'Import and visualize', pillNav, currentPillId: 'data', }) expect(wrapper.findComponent(AppSidebar).props()).toMatchObject({ panels, activeView: 'data', title: 'Tools', subtitle: 'Current file', floating: false, density: 'compact', }) expect(wrapper.get('[data-test="topbar-action"]').text()).toBe('Run') expect(wrapper.get('[data-test="import-action"]').text()).toBe('Upload') expect(wrapper.get('[data-test="content"]').text()).toBe('data') // WorkspaceControlsInternal receives exactly the scoped-slot payload the default slot sees. expect(wrapper.findComponent(WorkspaceControlsInternal).props('controls')).toMatchObject({ activeView: 'data', values: {}, }) }) it('keeps topbar navigation and sidebar panels synchronized through the internal split', async () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'Analysis', pillNav, panels, }, slots: { default: ({ activeView }: WorkspaceSlotProps) => h('main', { 'data-test': 'content' }, activeView), }, global: globalOptions, }) wrapper.findComponent(AppTopBar).vm.$emit('pill-select', { id: 'results', label: 'Results' }) await nextTick() expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('results') expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results') expect(wrapper.findComponent(WorkspaceSidebarInternal).props('sidebar')).toMatchObject({ activeView: 'results' }) expect(wrapper.get('[data-test="content"]').text()).toBe('results') expect(wrapper.emitted('update:activeView')).toEqual([['results']]) expect(wrapper.emitted('pill-select')).toEqual([[{ id: 'results', label: 'Results' }]]) }) it('relays sidebar collapse, toggle, and form events unchanged through WorkspaceSidebarInternal', async () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'Analysis', panels, forms: { data: { sections: [] }, }, }, global: globalOptions, }) const sidebar = wrapper.findComponent(AppSidebar) sidebar.vm.$emit('update:collapsed', true) sidebar.vm.$emit('update:toggle', 'import', false) sidebar.vm.$emit('section-action', 'import', 'refresh') sidebar.vm.$emit('form-submit', 'data', { value: 1 }) sidebar.vm.$emit('form-cancel', 'data') await nextTick() expect(wrapper.emitted('update:sidebarCollapsed')).toEqual([[true]]) expect(wrapper.emitted('update:toggle')).toEqual([['import', false]]) expect(wrapper.emitted('section-action')).toEqual([['import', 'refresh']]) expect(wrapper.emitted('form-submit')).toEqual([['data', { value: 1 }]]) expect(wrapper.emitted('form-cancel')).toEqual([['data']]) }) it('can render without a sidebar surface, omitting WorkspaceSidebarInternal entirely', () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'Simple Plugin', showSidebar: false, }, slots: { default: () => h('main', { 'data-test': 'content' }, 'Ready'), }, global: globalOptions, }) expect(wrapper.findComponent(WorkspaceSidebarInternal).exists()).toBe(false) expect(wrapper.findComponent(AppSidebar).exists()).toBe(false) expect(wrapper.findComponent(AppTopBar).props('title')).toBe('Simple Plugin') expect(wrapper.get('[data-test="content"]').text()).toBe('Ready') }) it('lets a consumer-supplied sidebar slot override WorkspaceSidebarInternal', () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'Custom Sidebar', panels, }, slots: { sidebar: () => h('aside', { 'data-test': 'custom-sidebar' }, 'Custom'), }, global: globalOptions, }) expect(wrapper.findComponent(WorkspaceSidebarInternal).exists()).toBe(false) expect(wrapper.findComponent(AppSidebar).exists()).toBe(false) expect(wrapper.get('[data-test="custom-sidebar"]').text()).toBe('Custom') }) it('lets a consumer-supplied topbar slot override WorkspaceShellInternal default AppTopBar rendering', () => { const wrapper = mount(PluginWorkspaceView, { props: { title: 'Custom Top Bar' }, slots: { topbar: () => h('header', { 'data-test': 'custom-topbar' }, 'Custom'), }, global: globalOptions, }) expect(wrapper.findComponent(AppTopBar).exists()).toBe(false) expect(wrapper.get('[data-test="custom-topbar"]').text()).toBe('Custom') }) })