mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 18:10:08 +00:00
CodeRabbit Generated Unit Tests: Add comprehensive unit tests for right panel components and stores
This commit is contained in:
committed by
GitHub
parent
a05d34c704
commit
364e662bc3
426
src/components/rightSidePanel/info/TabInfo.test.ts
Normal file
426
src/components/rightSidePanel/info/TabInfo.test.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import NodeHelpContent from '@/components/node/NodeHelpContent.vue'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
|
||||
import TabInfo from './TabInfo.vue'
|
||||
|
||||
// Mock the stores
|
||||
vi.mock('@/stores/nodeDefStore', () => ({
|
||||
useNodeDefStore: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/workspace/nodeHelpStore', () => ({
|
||||
useNodeHelpStore: vi.fn()
|
||||
}))
|
||||
|
||||
// Mock NodeHelpContent component
|
||||
vi.mock('@/components/node/NodeHelpContent.vue', () => ({
|
||||
default: {
|
||||
name: 'NodeHelpContent',
|
||||
template: '<div class="node-help-content">{{ node?.type }}</div>',
|
||||
props: ['node']
|
||||
}
|
||||
}))
|
||||
|
||||
describe('TabInfo', () => {
|
||||
let mockNodeDefStore: any
|
||||
let mockNodeHelpStore: any
|
||||
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
|
||||
mockNodeDefStore = {
|
||||
nodeDefsByName: {
|
||||
'KSampler': {
|
||||
name: 'KSampler',
|
||||
display_name: 'KSampler',
|
||||
description: 'Sampling node',
|
||||
category: 'sampling'
|
||||
},
|
||||
'CheckpointLoader': {
|
||||
name: 'CheckpointLoader',
|
||||
display_name: 'Load Checkpoint',
|
||||
description: 'Loads model checkpoint',
|
||||
category: 'loaders'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mockNodeHelpStore = {
|
||||
openHelp: vi.fn()
|
||||
}
|
||||
|
||||
vi.mocked(useNodeDefStore).mockReturnValue(mockNodeDefStore)
|
||||
vi.mocked(useNodeHelpStore).mockReturnValue(mockNodeHelpStore)
|
||||
})
|
||||
|
||||
const createMockNode = (type: string, id: number = 1): LGraphNode => ({
|
||||
id,
|
||||
type,
|
||||
title: `${type}_${id}`,
|
||||
properties: {},
|
||||
serialize: vi.fn(),
|
||||
configure: vi.fn()
|
||||
} as any)
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
return mount(TabInfo, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
stubs: {
|
||||
NodeHelpContent: true
|
||||
}
|
||||
},
|
||||
props: {
|
||||
nodes: [createMockNode('KSampler')],
|
||||
...props
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders successfully with single node', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders NodeHelpContent when node info exists', () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('renders with correct container styling', () => {
|
||||
const wrapper = mountComponent()
|
||||
const container = wrapper.find('div')
|
||||
|
||||
expect(container.classes()).toContain('rounded-lg')
|
||||
expect(container.classes()).toContain('bg-interface-surface')
|
||||
expect(container.classes()).toContain('p-3')
|
||||
})
|
||||
|
||||
it('does not render when node info is not available', () => {
|
||||
mockNodeDefStore.nodeDefsByName = {}
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('UnknownNode')]
|
||||
})
|
||||
|
||||
expect(wrapper.html()).toBe('<!--v-if-->')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Node Info Computation', () => {
|
||||
it('computes node info from first node in array', () => {
|
||||
const nodes = [
|
||||
createMockNode('KSampler', 1),
|
||||
createMockNode('CheckpointLoader', 2)
|
||||
]
|
||||
const wrapper = mountComponent({ nodes })
|
||||
|
||||
// Should use first node
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns node definition for valid node type', () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node')).toEqual(
|
||||
mockNodeDefStore.nodeDefsByName['KSampler']
|
||||
)
|
||||
})
|
||||
|
||||
it('returns undefined for invalid node type', () => {
|
||||
mockNodeDefStore.nodeDefsByName = {}
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('InvalidNode')]
|
||||
})
|
||||
|
||||
expect(wrapper.html()).toBe('<!--v-if-->')
|
||||
})
|
||||
|
||||
it('handles nodes array with single node', () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node')).toEqual(
|
||||
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Help Store Integration', () => {
|
||||
it('calls openHelp when nodeInfo exists', async () => {
|
||||
mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalled()
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
|
||||
mockNodeDefStore.nodeDefsByName['KSampler']
|
||||
)
|
||||
})
|
||||
|
||||
it('opens help immediately on mount', async () => {
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('updates help when node changes', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
// Change to different node
|
||||
await wrapper.setProps({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
|
||||
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
|
||||
)
|
||||
})
|
||||
|
||||
it('does not call openHelp when nodeInfo is undefined', async () => {
|
||||
mockNodeDefStore.nodeDefsByName = {}
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
mountComponent({
|
||||
nodes: [createMockNode('UnknownNode')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Props Handling', () => {
|
||||
it('accepts nodes prop as array', () => {
|
||||
const nodes = [
|
||||
createMockNode('KSampler', 1),
|
||||
createMockNode('CheckpointLoader', 2)
|
||||
]
|
||||
const wrapper = mountComponent({ nodes })
|
||||
|
||||
expect(wrapper.props('nodes')).toEqual(nodes)
|
||||
})
|
||||
|
||||
it('handles empty nodes array', () => {
|
||||
const wrapper = mountComponent({ nodes: [] })
|
||||
|
||||
expect(wrapper.html()).toBe('<!--v-if-->')
|
||||
})
|
||||
|
||||
it('updates when nodes prop changes', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await wrapper.setProps({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node')).toEqual(
|
||||
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles node with missing type', () => {
|
||||
const nodeWithoutType = {
|
||||
id: 1,
|
||||
title: 'Node',
|
||||
properties: {}
|
||||
} as any
|
||||
|
||||
const wrapper = mountComponent({
|
||||
nodes: [nodeWithoutType]
|
||||
})
|
||||
|
||||
expect(wrapper.html()).toBe('<!--v-if-->')
|
||||
})
|
||||
|
||||
it('handles rapid node switching', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await wrapper.setProps({
|
||||
nodes: [createMockNode(i % 2 === 0 ? 'KSampler' : 'CheckpointLoader')]
|
||||
})
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Should still be functional
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('handles nodes with special characters in type', () => {
|
||||
mockNodeDefStore.nodeDefsByName['Node-With-Dashes'] = {
|
||||
name: 'Node-With-Dashes',
|
||||
display_name: 'Node With Dashes'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('Node-With-Dashes')]
|
||||
})
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Reactivity', () => {
|
||||
it('recomputes nodeInfo when nodes prop changes', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
let helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node').name).toBe('KSampler')
|
||||
|
||||
await wrapper.setProps({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node').name).toBe('CheckpointLoader')
|
||||
})
|
||||
|
||||
it('recomputes nodeInfo when store updates', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
// Simulate store update
|
||||
mockNodeDefStore.nodeDefsByName['KSampler'] = {
|
||||
...mockNodeDefStore.nodeDefsByName['KSampler'],
|
||||
description: 'Updated description'
|
||||
}
|
||||
|
||||
await nextTick()
|
||||
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.props('node').description).toBe('Updated description')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Lifecycle', () => {
|
||||
it('calls openHelp on mount', async () => {
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('cleans up watchers on unmount', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
wrapper.unmount()
|
||||
|
||||
// Should not throw errors
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
it('handles remount correctly', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
wrapper.unmount()
|
||||
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
const wrapper2 = mountComponent({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
|
||||
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration Scenarios', () => {
|
||||
it('works in typical workflow: select node, view info', async () => {
|
||||
// User selects a node
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Info panel opens and displays help
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalled()
|
||||
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
|
||||
expect(helpContent.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('updates when user selects different node', async () => {
|
||||
const wrapper = mountComponent({
|
||||
nodes: [createMockNode('KSampler')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
mockNodeHelpStore.openHelp.mockClear()
|
||||
|
||||
// User selects different node
|
||||
await wrapper.setProps({
|
||||
nodes: [createMockNode('CheckpointLoader')]
|
||||
})
|
||||
|
||||
await nextTick()
|
||||
|
||||
// Help updates to new node
|
||||
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
|
||||
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
408
src/components/rightSidePanel/layout/RightPanelSection.test.ts
Normal file
408
src/components/rightSidePanel/layout/RightPanelSection.test.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import RightPanelSection from './RightPanelSection.vue'
|
||||
|
||||
describe('RightPanelSection', () => {
|
||||
beforeAll(() => {
|
||||
const app = { use: vi.fn() }
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}, slots = {}) => {
|
||||
return mount(RightPanelSection, {
|
||||
global: {
|
||||
plugins: [PrimeVue]
|
||||
},
|
||||
props: {
|
||||
label: 'Test Section',
|
||||
...props
|
||||
},
|
||||
slots
|
||||
})
|
||||
}
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders with default props', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('displays label from props', () => {
|
||||
const wrapper = mountComponent({ label: 'Custom Label' })
|
||||
const button = wrapper.find('button')
|
||||
expect(button.text()).toContain('Custom Label')
|
||||
})
|
||||
|
||||
it('renders label from slot when provided', () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
label: '<span class="custom-label">Slot Label</span>'
|
||||
}
|
||||
)
|
||||
const button = wrapper.find('button')
|
||||
expect(button.html()).toContain('custom-label')
|
||||
expect(button.text()).toContain('Slot Label')
|
||||
})
|
||||
|
||||
it('renders default slot content when expanded', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: '<div class="test-content">Test Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
// Content should be visible by default (not collapsed)
|
||||
expect(wrapper.html()).toContain('test-content')
|
||||
expect(wrapper.text()).toContain('Test Content')
|
||||
})
|
||||
|
||||
it('renders chevron icon in button', () => {
|
||||
const wrapper = mountComponent()
|
||||
const icon = wrapper.find('i.icon-\\[lucide--chevron-down\\]')
|
||||
expect(icon.exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Collapse/Expand Functionality', () => {
|
||||
it('starts in expanded state by default', () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.html()).toContain('content')
|
||||
})
|
||||
|
||||
it('starts in collapsed state when defaultCollapse is true', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{ defaultCollapse: true },
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
await nextTick()
|
||||
expect(wrapper.html()).not.toContain('content')
|
||||
})
|
||||
|
||||
it('toggles collapse state when button is clicked', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
const button = wrapper.find('button')
|
||||
|
||||
// Initially expanded
|
||||
expect(wrapper.html()).toContain('content')
|
||||
|
||||
// Click to collapse
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.html()).not.toContain('content')
|
||||
|
||||
// Click to expand again
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.html()).toContain('content')
|
||||
})
|
||||
|
||||
it('rotates chevron icon when collapsed', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
const icon = wrapper.find('i.icon-\\[lucide--chevron-down\\]')
|
||||
|
||||
// Initially not rotated (expanded state)
|
||||
expect(icon.classes()).not.toContain('rotate-90')
|
||||
|
||||
// Click to collapse
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// Should be rotated when collapsed
|
||||
expect(icon.classes()).toContain('rotate-90')
|
||||
})
|
||||
|
||||
it('emits update:collapse event when toggled', async () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('update:collapse')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:collapse')?.[0]).toEqual([true])
|
||||
})
|
||||
|
||||
it('supports v-model:collapse binding', async () => {
|
||||
const wrapper = mountComponent({ collapse: false })
|
||||
const button = wrapper.find('button')
|
||||
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('update:collapse')?.[0]).toEqual([true])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Reactivity', () => {
|
||||
it('reacts to defaultCollapse prop changes', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{ defaultCollapse: false },
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
// Initially expanded
|
||||
expect(wrapper.html()).toContain('content')
|
||||
|
||||
// Change to collapsed
|
||||
await wrapper.setProps({ defaultCollapse: true })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.html()).not.toContain('content')
|
||||
})
|
||||
|
||||
it('updates when collapse model value changes', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{ collapse: false },
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
|
||||
// Initially expanded
|
||||
expect(wrapper.html()).toContain('content')
|
||||
|
||||
// Programmatically collapse
|
||||
await wrapper.setProps({ collapse: true })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.html()).not.toContain('content')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling and Classes', () => {
|
||||
it('applies correct sticky header styles', () => {
|
||||
const wrapper = mountComponent()
|
||||
const header = wrapper.find('.sticky')
|
||||
|
||||
expect(header.exists()).toBe(true)
|
||||
expect(header.classes()).toContain('top-0')
|
||||
expect(header.classes()).toContain('z-10')
|
||||
})
|
||||
|
||||
it('applies button classes correctly', () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.classes()).toContain('group')
|
||||
expect(button.classes()).toContain('cursor-pointer')
|
||||
expect(button.classes()).toContain('w-full')
|
||||
})
|
||||
|
||||
it('applies transition class to chevron icon', () => {
|
||||
const wrapper = mountComponent()
|
||||
const icon = wrapper.find('i')
|
||||
|
||||
expect(icon.classes()).toContain('transition-all')
|
||||
})
|
||||
|
||||
it('applies hover styles to icon through group', () => {
|
||||
const wrapper = mountComponent()
|
||||
const icon = wrapper.find('i')
|
||||
|
||||
expect(icon.classes()).toContain('group-hover:text-base-foreground')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('button is keyboard accessible', () => {
|
||||
const wrapper = mountComponent()
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.element.tagName).toBe('BUTTON')
|
||||
})
|
||||
|
||||
it('maintains semantic HTML structure', () => {
|
||||
const wrapper = mountComponent()
|
||||
|
||||
// Should have a div container
|
||||
expect(wrapper.element.tagName).toBe('DIV')
|
||||
|
||||
// Should have a button for interaction
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('label text is accessible', () => {
|
||||
const wrapper = mountComponent({ label: 'Test Section' })
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.text()).toContain('Test Section')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles empty label gracefully', () => {
|
||||
const wrapper = mountComponent({ label: '' })
|
||||
const button = wrapper.find('button')
|
||||
|
||||
expect(button.exists()).toBe(true)
|
||||
expect(button.text().trim()).toBe('')
|
||||
})
|
||||
|
||||
it('handles undefined label', () => {
|
||||
const wrapper = mountComponent({ label: undefined })
|
||||
expect(wrapper.find('button').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('handles null default slot', () => {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('handles multiple rapid toggle clicks', async () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: '<div class="content">Content</div>'
|
||||
}
|
||||
)
|
||||
const button = wrapper.find('button')
|
||||
|
||||
// Rapidly click multiple times
|
||||
await button.trigger('click')
|
||||
await button.trigger('click')
|
||||
await button.trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// Should handle all clicks without errors
|
||||
expect(wrapper.emitted('update:collapse')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('handles very long label text', () => {
|
||||
const longLabel = 'A'.repeat(1000)
|
||||
const wrapper = mountComponent({ label: longLabel })
|
||||
const span = wrapper.find('span.line-clamp-2')
|
||||
|
||||
expect(span.exists()).toBe(true)
|
||||
expect(span.text()).toContain('A')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration Scenarios', () => {
|
||||
it('works with multiple sections in sequence', () => {
|
||||
const wrapper1 = mountComponent({ label: 'Section 1' })
|
||||
const wrapper2 = mountComponent({ label: 'Section 2' })
|
||||
|
||||
expect(wrapper1.text()).toContain('Section 1')
|
||||
expect(wrapper2.text()).toContain('Section 2')
|
||||
})
|
||||
|
||||
it('maintains independent state between instances', async () => {
|
||||
const wrapper1 = mountComponent(
|
||||
{ label: 'Section 1' },
|
||||
{ default: '<div>Content 1</div>' }
|
||||
)
|
||||
const wrapper2 = mountComponent(
|
||||
{ label: 'Section 2' },
|
||||
{ default: '<div>Content 2</div>' }
|
||||
)
|
||||
|
||||
// Collapse first section
|
||||
await wrapper1.find('button').trigger('click')
|
||||
await nextTick()
|
||||
|
||||
// First should be collapsed
|
||||
expect(wrapper1.html()).not.toContain('Content 1')
|
||||
// Second should remain expanded
|
||||
expect(wrapper2.html()).toContain('Content 2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Slot Behavior', () => {
|
||||
it('renders complex nested content', () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: `
|
||||
<div class="outer">
|
||||
<div class="inner">
|
||||
<span class="text">Nested Content</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.html()).toContain('outer')
|
||||
expect(wrapper.html()).toContain('inner')
|
||||
expect(wrapper.text()).toContain('Nested Content')
|
||||
})
|
||||
|
||||
it('renders multiple child elements', () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: `
|
||||
<div class="child-1">Child 1</div>
|
||||
<div class="child-2">Child 2</div>
|
||||
<div class="child-3">Child 3</div>
|
||||
`
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.html()).toContain('child-1')
|
||||
expect(wrapper.html()).toContain('child-2')
|
||||
expect(wrapper.html()).toContain('child-3')
|
||||
})
|
||||
|
||||
it('preserves slot content styling', () => {
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{
|
||||
default: '<div style="color: red;" class="styled-content">Styled</div>'
|
||||
}
|
||||
)
|
||||
|
||||
expect(wrapper.html()).toContain('styled-content')
|
||||
expect(wrapper.html()).toContain('color: red')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Performance', () => {
|
||||
it('handles rapid mount/unmount cycles', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('efficiently handles large content in slot', () => {
|
||||
const largeContent = Array.from({ length: 100 }, (_, i) =>
|
||||
`<div class="item-${i}">Item ${i}</div>`
|
||||
).join('')
|
||||
|
||||
const wrapper = mountComponent(
|
||||
{},
|
||||
{ default: largeContent }
|
||||
)
|
||||
|
||||
expect(wrapper.exists()).toBe(true)
|
||||
expect(wrapper.html()).toContain('item-0')
|
||||
expect(wrapper.html()).toContain('item-99')
|
||||
})
|
||||
})
|
||||
})
|
||||
869
src/index.test.ts
Normal file
869
src/index.test.ts
Normal file
@@ -0,0 +1,869 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { getRequiredEnv, getOptionalEnv, validateEnv } from './index';
|
||||
|
||||
describe('Environment Variable Utilities', () => {
|
||||
// Store original env to restore after tests
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear environment before each test
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment after each test
|
||||
process.env = originalEnv;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getRequiredEnv', () => {
|
||||
describe('happy path', () => {
|
||||
it('should return the value when environment variable exists', () => {
|
||||
process.env.TEST_VAR = 'test-value';
|
||||
const result = getRequiredEnv('TEST_VAR');
|
||||
expect(result).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should return value with special characters', () => {
|
||||
process.env.SPECIAL_VAR = 'value-with-@#$%^&*()_+={}[]|:;"<>?,./~`';
|
||||
const result = getRequiredEnv('SPECIAL_VAR');
|
||||
expect(result).toBe('value-with-@#$%^&*()_+={}[]|:;"<>?,./~`');
|
||||
});
|
||||
|
||||
it('should return value with whitespace', () => {
|
||||
process.env.WHITESPACE_VAR = ' value with spaces ';
|
||||
const result = getRequiredEnv('WHITESPACE_VAR');
|
||||
expect(result).toBe(' value with spaces ');
|
||||
});
|
||||
|
||||
it('should return multiline value', () => {
|
||||
process.env.MULTILINE_VAR = 'line1\nline2\nline3';
|
||||
const result = getRequiredEnv('MULTILINE_VAR');
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should return very long value', () => {
|
||||
const longValue = 'a'.repeat(10000);
|
||||
process.env.LONG_VAR = longValue;
|
||||
const result = getRequiredEnv('LONG_VAR');
|
||||
expect(result).toBe(longValue);
|
||||
});
|
||||
|
||||
it('should return numeric string value', () => {
|
||||
process.env.NUMERIC_VAR = '12345';
|
||||
const result = getRequiredEnv('NUMERIC_VAR');
|
||||
expect(result).toBe('12345');
|
||||
});
|
||||
|
||||
it('should return boolean string value', () => {
|
||||
process.env.BOOL_VAR = 'true';
|
||||
const result = getRequiredEnv('BOOL_VAR');
|
||||
expect(result).toBe('true');
|
||||
});
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
process.env.UNICODE_VAR = '你好世界🌍🚀';
|
||||
const result = getRequiredEnv('UNICODE_VAR');
|
||||
expect(result).toBe('你好世界🌍🚀');
|
||||
});
|
||||
|
||||
it('should handle URL values', () => {
|
||||
process.env.URL_VAR = 'https://example.com:8080/path?query=value&another=test#fragment';
|
||||
const result = getRequiredEnv('URL_VAR');
|
||||
expect(result).toBe('https://example.com:8080/path?query=value&another=test#fragment');
|
||||
});
|
||||
|
||||
it('should handle JSON string values', () => {
|
||||
const jsonValue = '{"key":"value","nested":{"array":[1,2,3]}}';
|
||||
process.env.JSON_VAR = jsonValue;
|
||||
const result = getRequiredEnv('JSON_VAR');
|
||||
expect(result).toBe(jsonValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should throw error when variable is undefined', () => {
|
||||
delete process.env.UNDEFINED_VAR;
|
||||
expect(() => getRequiredEnv('UNDEFINED_VAR')).toThrow();
|
||||
});
|
||||
|
||||
it('should throw error with descriptive message for missing variable', () => {
|
||||
delete process.env.MISSING_VAR;
|
||||
expect(() => getRequiredEnv('MISSING_VAR')).toThrow('Environment variable MISSING_VAR is required but not set');
|
||||
});
|
||||
|
||||
it('should throw error when variable is empty string', () => {
|
||||
process.env.EMPTY_VAR = '';
|
||||
expect(() => getRequiredEnv('EMPTY_VAR')).toThrow('Environment variable EMPTY_VAR is required but not set');
|
||||
});
|
||||
|
||||
it('should throw error when variable is only whitespace', () => {
|
||||
process.env.WHITESPACE_ONLY_VAR = ' ';
|
||||
expect(() => getRequiredEnv('WHITESPACE_ONLY_VAR')).toThrow();
|
||||
});
|
||||
|
||||
it('should handle variable name with special characters', () => {
|
||||
process.env['VAR_WITH-DASH'] = 'value';
|
||||
const result = getRequiredEnv('VAR_WITH-DASH');
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should handle variable name with numbers', () => {
|
||||
process.env.VAR123 = 'value';
|
||||
const result = getRequiredEnv('VAR123');
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should handle single character variable name', () => {
|
||||
process.env.X = 'value';
|
||||
const result = getRequiredEnv('X');
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should handle very long variable name', () => {
|
||||
const longName = 'VAR_' + 'A'.repeat(200);
|
||||
process.env[longName] = 'value';
|
||||
const result = getRequiredEnv(longName);
|
||||
expect(result).toBe('value');
|
||||
});
|
||||
|
||||
it('should return "0" without throwing (zero is a valid value)', () => {
|
||||
process.env.ZERO_VAR = '0';
|
||||
const result = getRequiredEnv('ZERO_VAR');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should return "false" without throwing (false string is valid)', () => {
|
||||
process.env.FALSE_VAR = 'false';
|
||||
const result = getRequiredEnv('FALSE_VAR');
|
||||
expect(result).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error conditions', () => {
|
||||
it('should throw TypeError when varName is not a string', () => {
|
||||
expect(() => getRequiredEnv(null as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is undefined', () => {
|
||||
expect(() => getRequiredEnv(undefined as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is a number', () => {
|
||||
expect(() => getRequiredEnv(123 as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is an object', () => {
|
||||
expect(() => getRequiredEnv({} as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is an array', () => {
|
||||
expect(() => getRequiredEnv([] as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when varName is empty string', () => {
|
||||
expect(() => getRequiredEnv('')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should work correctly when called multiple times for same variable', () => {
|
||||
process.env.MULTI_CALL_VAR = 'value';
|
||||
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
|
||||
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
|
||||
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
|
||||
});
|
||||
|
||||
it('should work correctly when called for different variables', () => {
|
||||
process.env.VAR_A = 'value-a';
|
||||
process.env.VAR_B = 'value-b';
|
||||
process.env.VAR_C = 'value-c';
|
||||
|
||||
expect(getRequiredEnv('VAR_A')).toBe('value-a');
|
||||
expect(getRequiredEnv('VAR_B')).toBe('value-b');
|
||||
expect(getRequiredEnv('VAR_C')).toBe('value-c');
|
||||
});
|
||||
|
||||
it('should reflect environment changes between calls', () => {
|
||||
process.env.DYNAMIC_VAR = 'initial';
|
||||
expect(getRequiredEnv('DYNAMIC_VAR')).toBe('initial');
|
||||
|
||||
process.env.DYNAMIC_VAR = 'updated';
|
||||
expect(getRequiredEnv('DYNAMIC_VAR')).toBe('updated');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOptionalEnv', () => {
|
||||
describe('happy path', () => {
|
||||
it('should return the value when environment variable exists', () => {
|
||||
process.env.OPTIONAL_VAR = 'optional-value';
|
||||
const result = getOptionalEnv('OPTIONAL_VAR');
|
||||
expect(result).toBe('optional-value');
|
||||
});
|
||||
|
||||
it('should return undefined when variable does not exist', () => {
|
||||
delete process.env.NONEXISTENT_VAR;
|
||||
const result = getOptionalEnv('NONEXISTENT_VAR');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return default value when variable does not exist', () => {
|
||||
delete process.env.DEFAULT_VAR;
|
||||
const result = getOptionalEnv('DEFAULT_VAR', 'default-value');
|
||||
expect(result).toBe('default-value');
|
||||
});
|
||||
|
||||
it('should return actual value over default when variable exists', () => {
|
||||
process.env.PRIORITY_VAR = 'actual-value';
|
||||
const result = getOptionalEnv('PRIORITY_VAR', 'default-value');
|
||||
expect(result).toBe('actual-value');
|
||||
});
|
||||
|
||||
it('should handle special characters in value', () => {
|
||||
process.env.SPECIAL_OPTIONAL = '@#$%^&*()_+{}[]|:;"<>?,./';
|
||||
const result = getOptionalEnv('SPECIAL_OPTIONAL');
|
||||
expect(result).toBe('@#$%^&*()_+{}[]|:;"<>?,./');
|
||||
});
|
||||
|
||||
it('should handle whitespace in value', () => {
|
||||
process.env.WHITESPACE_OPTIONAL = ' spaced value ';
|
||||
const result = getOptionalEnv('WHITESPACE_OPTIONAL');
|
||||
expect(result).toBe(' spaced value ');
|
||||
});
|
||||
|
||||
it('should handle multiline value', () => {
|
||||
process.env.MULTILINE_OPTIONAL = 'line1\nline2\nline3';
|
||||
const result = getOptionalEnv('MULTILINE_OPTIONAL');
|
||||
expect(result).toBe('line1\nline2\nline3');
|
||||
});
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
process.env.UNICODE_OPTIONAL = '测试🎉';
|
||||
const result = getOptionalEnv('UNICODE_OPTIONAL');
|
||||
expect(result).toBe('测试🎉');
|
||||
});
|
||||
|
||||
it('should handle URL values', () => {
|
||||
process.env.URL_OPTIONAL = 'https://api.example.com/v1/endpoint';
|
||||
const result = getOptionalEnv('URL_OPTIONAL');
|
||||
expect(result).toBe('https://api.example.com/v1/endpoint');
|
||||
});
|
||||
|
||||
it('should return numeric string', () => {
|
||||
process.env.NUM_OPTIONAL = '42';
|
||||
const result = getOptionalEnv('NUM_OPTIONAL');
|
||||
expect(result).toBe('42');
|
||||
});
|
||||
|
||||
it('should return "0" as valid value', () => {
|
||||
process.env.ZERO_OPTIONAL = '0';
|
||||
const result = getOptionalEnv('ZERO_OPTIONAL');
|
||||
expect(result).toBe('0');
|
||||
});
|
||||
|
||||
it('should return "false" as valid value', () => {
|
||||
process.env.FALSE_OPTIONAL = 'false';
|
||||
const result = getOptionalEnv('FALSE_OPTIONAL');
|
||||
expect(result).toBe('false');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return undefined for empty string when no default provided', () => {
|
||||
process.env.EMPTY_OPTIONAL = '';
|
||||
const result = getOptionalEnv('EMPTY_OPTIONAL');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return default for empty string when default provided', () => {
|
||||
process.env.EMPTY_WITH_DEFAULT = '';
|
||||
const result = getOptionalEnv('EMPTY_WITH_DEFAULT', 'fallback');
|
||||
expect(result).toBe('fallback');
|
||||
});
|
||||
|
||||
it('should return undefined for whitespace-only value', () => {
|
||||
process.env.WHITESPACE_ONLY_OPTIONAL = ' ';
|
||||
const result = getOptionalEnv('WHITESPACE_ONLY_OPTIONAL');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null as default value', () => {
|
||||
delete process.env.NULL_DEFAULT_VAR;
|
||||
const result = getOptionalEnv('NULL_DEFAULT_VAR', null as any);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty string as default value', () => {
|
||||
delete process.env.EMPTY_DEFAULT_VAR;
|
||||
const result = getOptionalEnv('EMPTY_DEFAULT_VAR', '');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle numeric default value', () => {
|
||||
delete process.env.NUMERIC_DEFAULT_VAR;
|
||||
const result = getOptionalEnv('NUMERIC_DEFAULT_VAR', 123 as any);
|
||||
expect(result).toBe(123);
|
||||
});
|
||||
|
||||
it('should handle boolean default value', () => {
|
||||
delete process.env.BOOLEAN_DEFAULT_VAR;
|
||||
const result = getOptionalEnv('BOOLEAN_DEFAULT_VAR', true as any);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle object as default value', () => {
|
||||
delete process.env.OBJECT_DEFAULT_VAR;
|
||||
const defaultObj = { key: 'value' };
|
||||
const result = getOptionalEnv('OBJECT_DEFAULT_VAR', defaultObj as any);
|
||||
expect(result).toBe(defaultObj);
|
||||
});
|
||||
|
||||
it('should handle array as default value', () => {
|
||||
delete process.env.ARRAY_DEFAULT_VAR;
|
||||
const defaultArray = [1, 2, 3];
|
||||
const result = getOptionalEnv('ARRAY_DEFAULT_VAR', defaultArray as any);
|
||||
expect(result).toBe(defaultArray);
|
||||
});
|
||||
|
||||
it('should handle very long default value', () => {
|
||||
delete process.env.LONG_DEFAULT_VAR;
|
||||
const longDefault = 'x'.repeat(10000);
|
||||
const result = getOptionalEnv('LONG_DEFAULT_VAR', longDefault);
|
||||
expect(result).toBe(longDefault);
|
||||
});
|
||||
|
||||
it('should handle special characters in default value', () => {
|
||||
delete process.env.SPECIAL_DEFAULT_VAR;
|
||||
const result = getOptionalEnv('SPECIAL_DEFAULT_VAR', '@#$%^&*()');
|
||||
expect(result).toBe('@#$%^&*()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('error conditions', () => {
|
||||
it('should throw TypeError when varName is not a string', () => {
|
||||
expect(() => getOptionalEnv(null as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is undefined', () => {
|
||||
expect(() => getOptionalEnv(undefined as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is a number', () => {
|
||||
expect(() => getOptionalEnv(123 as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varName is an object', () => {
|
||||
expect(() => getOptionalEnv({} as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when varName is empty string', () => {
|
||||
expect(() => getOptionalEnv('')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should work correctly when called multiple times for same variable', () => {
|
||||
process.env.MULTI_OPTIONAL = 'value';
|
||||
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
|
||||
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
|
||||
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
|
||||
});
|
||||
|
||||
it('should work correctly with different default values on multiple calls', () => {
|
||||
delete process.env.DEFAULT_MULTI;
|
||||
expect(getOptionalEnv('DEFAULT_MULTI', 'default1')).toBe('default1');
|
||||
expect(getOptionalEnv('DEFAULT_MULTI', 'default2')).toBe('default2');
|
||||
expect(getOptionalEnv('DEFAULT_MULTI')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should reflect environment changes between calls', () => {
|
||||
delete process.env.CHANGING_OPTIONAL;
|
||||
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('default');
|
||||
|
||||
process.env.CHANGING_OPTIONAL = 'new-value';
|
||||
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('new-value');
|
||||
|
||||
delete process.env.CHANGING_OPTIONAL;
|
||||
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('default');
|
||||
});
|
||||
|
||||
it('should work alongside getRequiredEnv for different variables', () => {
|
||||
process.env.REQUIRED_VAR = 'required';
|
||||
process.env.OPTIONAL_VAR = 'optional';
|
||||
|
||||
expect(getRequiredEnv('REQUIRED_VAR')).toBe('required');
|
||||
expect(getOptionalEnv('OPTIONAL_VAR')).toBe('optional');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEnv', () => {
|
||||
describe('happy path', () => {
|
||||
it('should validate all required variables successfully', () => {
|
||||
process.env.VAR1 = 'value1';
|
||||
process.env.VAR2 = 'value2';
|
||||
process.env.VAR3 = 'value3';
|
||||
|
||||
expect(() => validateEnv(['VAR1', 'VAR2', 'VAR3'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate single required variable', () => {
|
||||
process.env.SINGLE_VAR = 'value';
|
||||
expect(() => validateEnv(['SINGLE_VAR'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate empty array without throwing', () => {
|
||||
expect(() => validateEnv([])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate variables with special characters in values', () => {
|
||||
process.env.SPECIAL1 = 'value!@#$%';
|
||||
process.env.SPECIAL2 = 'value^&*()';
|
||||
expect(() => validateEnv(['SPECIAL1', 'SPECIAL2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate variables with numeric values', () => {
|
||||
process.env.NUM1 = '123';
|
||||
process.env.NUM2 = '456.789';
|
||||
expect(() => validateEnv(['NUM1', 'NUM2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate variables with boolean string values', () => {
|
||||
process.env.BOOL1 = 'true';
|
||||
process.env.BOOL2 = 'false';
|
||||
expect(() => validateEnv(['BOOL1', 'BOOL2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate variables with URL values', () => {
|
||||
process.env.URL1 = 'https://example.com';
|
||||
process.env.URL2 = 'http://localhost:3000';
|
||||
expect(() => validateEnv(['URL1', 'URL2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate variables with JSON string values', () => {
|
||||
process.env.JSON1 = '{"key":"value"}';
|
||||
process.env.JSON2 = '[1,2,3]';
|
||||
expect(() => validateEnv(['JSON1', 'JSON2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate many variables at once', () => {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
process.env[`VAR_${i}`] = `value_${i}`;
|
||||
}
|
||||
const varNames = Array.from({ length: 50 }, (_, i) => `VAR_${i}`);
|
||||
expect(() => validateEnv(varNames)).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should throw when one required variable is missing', () => {
|
||||
process.env.PRESENT = 'value';
|
||||
delete process.env.MISSING;
|
||||
|
||||
expect(() => validateEnv(['PRESENT', 'MISSING'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when multiple required variables are missing', () => {
|
||||
delete process.env.MISSING1;
|
||||
delete process.env.MISSING2;
|
||||
|
||||
expect(() => validateEnv(['MISSING1', 'MISSING2'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when variable is empty string', () => {
|
||||
process.env.EMPTY = '';
|
||||
expect(() => validateEnv(['EMPTY'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when variable is whitespace only', () => {
|
||||
process.env.WHITESPACE = ' ';
|
||||
expect(() => validateEnv(['WHITESPACE'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw descriptive error for missing variables', () => {
|
||||
delete process.env.MISSING_VAR;
|
||||
expect(() => validateEnv(['MISSING_VAR'])).toThrow('MISSING_VAR');
|
||||
});
|
||||
|
||||
it('should handle duplicate variable names in array', () => {
|
||||
process.env.DUPLICATE = 'value';
|
||||
expect(() => validateEnv(['DUPLICATE', 'DUPLICATE'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle variable names with special characters', () => {
|
||||
process.env['VAR-WITH-DASH'] = 'value';
|
||||
process.env['VAR_WITH_UNDERSCORE'] = 'value';
|
||||
expect(() => validateEnv(['VAR-WITH-DASH', 'VAR_WITH_UNDERSCORE'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate single character variable names', () => {
|
||||
process.env.A = 'value';
|
||||
process.env.B = 'value';
|
||||
expect(() => validateEnv(['A', 'B'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should validate very long variable names', () => {
|
||||
const longName1 = 'VAR_' + 'A'.repeat(100);
|
||||
const longName2 = 'VAR_' + 'B'.repeat(100);
|
||||
process.env[longName1] = 'value';
|
||||
process.env[longName2] = 'value';
|
||||
expect(() => validateEnv([longName1, longName2])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept "0" as valid value', () => {
|
||||
process.env.ZERO = '0';
|
||||
expect(() => validateEnv(['ZERO'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should accept "false" as valid value', () => {
|
||||
process.env.FALSE_STR = 'false';
|
||||
expect(() => validateEnv(['FALSE_STR'])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error conditions', () => {
|
||||
it('should throw TypeError when varNames is not an array', () => {
|
||||
expect(() => validateEnv(null as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varNames is undefined', () => {
|
||||
expect(() => validateEnv(undefined as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varNames is a string', () => {
|
||||
expect(() => validateEnv('VAR' as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varNames is a number', () => {
|
||||
expect(() => validateEnv(123 as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw TypeError when varNames is an object', () => {
|
||||
expect(() => validateEnv({} as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains non-string elements', () => {
|
||||
expect(() => validateEnv([123, 'VAR'] as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains null', () => {
|
||||
expect(() => validateEnv([null, 'VAR'] as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains undefined', () => {
|
||||
expect(() => validateEnv([undefined, 'VAR'] as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains objects', () => {
|
||||
expect(() => validateEnv([{}, 'VAR'] as any)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains empty string', () => {
|
||||
expect(() => validateEnv(['', 'VAR'])).toThrow();
|
||||
});
|
||||
|
||||
it('should throw when array contains whitespace-only string', () => {
|
||||
expect(() => validateEnv([' ', 'VAR'])).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('error messages', () => {
|
||||
it('should include all missing variable names in error message', () => {
|
||||
delete process.env.MISSING1;
|
||||
delete process.env.MISSING2;
|
||||
delete process.env.MISSING3;
|
||||
|
||||
try {
|
||||
validateEnv(['MISSING1', 'MISSING2', 'MISSING3']);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain('MISSING1');
|
||||
expect(error.message).toContain('MISSING2');
|
||||
expect(error.message).toContain('MISSING3');
|
||||
}
|
||||
});
|
||||
|
||||
it('should not include present variables in error message', () => {
|
||||
process.env.PRESENT = 'value';
|
||||
delete process.env.MISSING;
|
||||
|
||||
try {
|
||||
validateEnv(['PRESENT', 'MISSING']);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error.message).not.toContain('PRESENT');
|
||||
expect(error.message).toContain('MISSING');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw clear error when no variables are provided but required', () => {
|
||||
try {
|
||||
validateEnv(null as any);
|
||||
expect.fail('Should have thrown an error');
|
||||
} catch (error) {
|
||||
expect(error).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration scenarios', () => {
|
||||
it('should work correctly when called multiple times', () => {
|
||||
process.env.VAR1 = 'value1';
|
||||
process.env.VAR2 = 'value2';
|
||||
|
||||
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
|
||||
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
|
||||
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should reflect environment changes between calls', () => {
|
||||
process.env.DYNAMIC = 'value';
|
||||
expect(() => validateEnv(['DYNAMIC'])).not.toThrow();
|
||||
|
||||
delete process.env.DYNAMIC;
|
||||
expect(() => validateEnv(['DYNAMIC'])).toThrow();
|
||||
|
||||
process.env.DYNAMIC = 'new-value';
|
||||
expect(() => validateEnv(['DYNAMIC'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should work with getRequiredEnv and getOptionalEnv', () => {
|
||||
process.env.REQUIRED1 = 'value1';
|
||||
process.env.REQUIRED2 = 'value2';
|
||||
process.env.OPTIONAL1 = 'value3';
|
||||
|
||||
expect(() => validateEnv(['REQUIRED1', 'REQUIRED2'])).not.toThrow();
|
||||
expect(getRequiredEnv('REQUIRED1')).toBe('value1');
|
||||
expect(getRequiredEnv('REQUIRED2')).toBe('value2');
|
||||
expect(getOptionalEnv('OPTIONAL1')).toBe('value3');
|
||||
});
|
||||
|
||||
it('should validate subset of environment variables', () => {
|
||||
process.env.VAR1 = 'value1';
|
||||
process.env.VAR2 = 'value2';
|
||||
process.env.VAR3 = 'value3';
|
||||
process.env.VAR4 = 'value4';
|
||||
|
||||
expect(() => validateEnv(['VAR1', 'VAR3'])).not.toThrow();
|
||||
expect(() => validateEnv(['VAR2', 'VAR4'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle validation of overlapping sets', () => {
|
||||
process.env.A = 'valueA';
|
||||
process.env.B = 'valueB';
|
||||
process.env.C = 'valueC';
|
||||
|
||||
expect(() => validateEnv(['A', 'B'])).not.toThrow();
|
||||
expect(() => validateEnv(['B', 'C'])).not.toThrow();
|
||||
expect(() => validateEnv(['A', 'C'])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('performance and stress tests', () => {
|
||||
it('should handle validation of large number of variables efficiently', () => {
|
||||
const varCount = 1000;
|
||||
for (let i = 0; i < varCount; i++) {
|
||||
process.env[`PERF_VAR_${i}`] = `value_${i}`;
|
||||
}
|
||||
|
||||
const varNames = Array.from({ length: varCount }, (_, i) => `PERF_VAR_${i}`);
|
||||
const startTime = Date.now();
|
||||
expect(() => validateEnv(varNames)).not.toThrow();
|
||||
const endTime = Date.now();
|
||||
|
||||
// Should complete in reasonable time (less than 1 second for 1000 vars)
|
||||
expect(endTime - startTime).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it('should handle validation with very long variable values', () => {
|
||||
const longValue = 'x'.repeat(100000);
|
||||
process.env.LONG_VALUE_VAR = longValue;
|
||||
expect(() => validateEnv(['LONG_VALUE_VAR'])).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-function integration tests', () => {
|
||||
it('should use all three functions together in typical workflow', () => {
|
||||
process.env.DATABASE_URL = 'postgres://localhost:5432/mydb';
|
||||
process.env.API_KEY = 'secret-key-123';
|
||||
process.env.DEBUG_MODE = 'true';
|
||||
process.env.OPTIONAL_FEATURE = 'enabled';
|
||||
|
||||
// Validate required vars
|
||||
expect(() => validateEnv(['DATABASE_URL', 'API_KEY', 'DEBUG_MODE'])).not.toThrow();
|
||||
|
||||
// Get required vars
|
||||
const dbUrl = getRequiredEnv('DATABASE_URL');
|
||||
const apiKey = getRequiredEnv('API_KEY');
|
||||
|
||||
// Get optional vars
|
||||
const optionalFeature = getOptionalEnv('OPTIONAL_FEATURE', 'disabled');
|
||||
const missingFeature = getOptionalEnv('MISSING_FEATURE', 'default');
|
||||
|
||||
expect(dbUrl).toBe('postgres://localhost:5432/mydb');
|
||||
expect(apiKey).toBe('secret-key-123');
|
||||
expect(optionalFeature).toBe('enabled');
|
||||
expect(missingFeature).toBe('default');
|
||||
});
|
||||
|
||||
it('should handle mixed scenarios with some variables present and some missing', () => {
|
||||
process.env.PRESENT1 = 'value1';
|
||||
process.env.PRESENT2 = 'value2';
|
||||
delete process.env.MISSING1;
|
||||
delete process.env.MISSING2;
|
||||
|
||||
expect(() => validateEnv(['PRESENT1', 'PRESENT2'])).not.toThrow();
|
||||
expect(getRequiredEnv('PRESENT1')).toBe('value1');
|
||||
expect(getOptionalEnv('MISSING1', 'default')).toBe('default');
|
||||
expect(() => getRequiredEnv('MISSING2')).toThrow();
|
||||
});
|
||||
|
||||
it('should handle environment initialization pattern', () => {
|
||||
// Simulate loading environment variables
|
||||
const requiredVars = ['APP_NAME', 'PORT', 'NODE_ENV'];
|
||||
|
||||
process.env.APP_NAME = 'MyApp';
|
||||
process.env.PORT = '3000';
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
// Validate all required vars at startup
|
||||
expect(() => validateEnv(requiredVars)).not.toThrow();
|
||||
|
||||
// Load individual vars
|
||||
const appName = getRequiredEnv('APP_NAME');
|
||||
const port = getRequiredEnv('PORT');
|
||||
const nodeEnv = getRequiredEnv('NODE_ENV');
|
||||
const logLevel = getOptionalEnv('LOG_LEVEL', 'info');
|
||||
|
||||
expect(appName).toBe('MyApp');
|
||||
expect(port).toBe('3000');
|
||||
expect(nodeEnv).toBe('development');
|
||||
expect(logLevel).toBe('info');
|
||||
});
|
||||
});
|
||||
|
||||
describe('type safety and TypeScript integration', () => {
|
||||
it('should work with TypeScript type inference for required env', () => {
|
||||
process.env.TYPED_VAR = 'typed-value';
|
||||
const value: string = getRequiredEnv('TYPED_VAR');
|
||||
expect(value).toBe('typed-value');
|
||||
});
|
||||
|
||||
it('should work with TypeScript type inference for optional env', () => {
|
||||
process.env.TYPED_OPTIONAL = 'typed-optional-value';
|
||||
const value: string | undefined = getOptionalEnv('TYPED_OPTIONAL');
|
||||
expect(value).toBe('typed-optional-value');
|
||||
});
|
||||
|
||||
it('should work with TypeScript type inference for optional env with default', () => {
|
||||
delete process.env.TYPED_WITH_DEFAULT;
|
||||
const value: string = getOptionalEnv('TYPED_WITH_DEFAULT', 'default-value')!;
|
||||
expect(value).toBe('default-value');
|
||||
});
|
||||
|
||||
it('should handle validateEnv with readonly arrays', () => {
|
||||
process.env.READONLY1 = 'value1';
|
||||
process.env.READONLY2 = 'value2';
|
||||
const vars: readonly string[] = ['READONLY1', 'READONLY2'] as const;
|
||||
expect(() => validateEnv(vars as string[])).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world scenario tests', () => {
|
||||
it('should handle typical database configuration', () => {
|
||||
process.env.DB_HOST = 'localhost';
|
||||
process.env.DB_PORT = '5432';
|
||||
process.env.DB_NAME = 'myapp';
|
||||
process.env.DB_USER = 'admin';
|
||||
process.env.DB_PASSWORD = 'secret123';
|
||||
|
||||
const dbConfig = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
|
||||
expect(() => validateEnv(dbConfig)).not.toThrow();
|
||||
|
||||
const host = getRequiredEnv('DB_HOST');
|
||||
const port = getRequiredEnv('DB_PORT');
|
||||
const dbName = getRequiredEnv('DB_NAME');
|
||||
const sslMode = getOptionalEnv('DB_SSL_MODE', 'prefer');
|
||||
|
||||
expect(host).toBe('localhost');
|
||||
expect(port).toBe('5432');
|
||||
expect(dbName).toBe('myapp');
|
||||
expect(sslMode).toBe('prefer');
|
||||
});
|
||||
|
||||
it('should handle API service configuration', () => {
|
||||
process.env.API_BASE_URL = 'https://api.example.com';
|
||||
process.env.API_KEY = 'key-12345';
|
||||
process.env.API_TIMEOUT = '30000';
|
||||
process.env.API_RETRY_COUNT = '3';
|
||||
|
||||
expect(() => validateEnv(['API_BASE_URL', 'API_KEY'])).not.toThrow();
|
||||
|
||||
const baseUrl = getRequiredEnv('API_BASE_URL');
|
||||
const apiKey = getRequiredEnv('API_KEY');
|
||||
const timeout = getOptionalEnv('API_TIMEOUT', '5000');
|
||||
const retryCount = getOptionalEnv('API_RETRY_COUNT', '1');
|
||||
const rateLimit = getOptionalEnv('API_RATE_LIMIT');
|
||||
|
||||
expect(baseUrl).toBe('https://api.example.com');
|
||||
expect(apiKey).toBe('key-12345');
|
||||
expect(timeout).toBe('30000');
|
||||
expect(retryCount).toBe('3');
|
||||
expect(rateLimit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle AWS credentials configuration', () => {
|
||||
process.env.AWS_REGION = 'us-east-1';
|
||||
process.env.AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE';
|
||||
process.env.AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
|
||||
|
||||
const awsVars = ['AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'];
|
||||
expect(() => validateEnv(awsVars)).not.toThrow();
|
||||
|
||||
const region = getRequiredEnv('AWS_REGION');
|
||||
const accessKeyId = getRequiredEnv('AWS_ACCESS_KEY_ID');
|
||||
const sessionToken = getOptionalEnv('AWS_SESSION_TOKEN');
|
||||
|
||||
expect(region).toBe('us-east-1');
|
||||
expect(accessKeyId).toBe('AKIAIOSFODNN7EXAMPLE');
|
||||
expect(sessionToken).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle feature flags configuration', () => {
|
||||
process.env.FEATURE_NEW_UI = 'true';
|
||||
process.env.FEATURE_BETA_API = 'false';
|
||||
process.env.FEATURE_ANALYTICS = 'enabled';
|
||||
|
||||
const newUI = getOptionalEnv('FEATURE_NEW_UI', 'false');
|
||||
const betaAPI = getOptionalEnv('FEATURE_BETA_API', 'false');
|
||||
const analytics = getOptionalEnv('FEATURE_ANALYTICS', 'disabled');
|
||||
const darkMode = getOptionalEnv('FEATURE_DARK_MODE', 'auto');
|
||||
|
||||
expect(newUI).toBe('true');
|
||||
expect(betaAPI).toBe('false');
|
||||
expect(analytics).toBe('enabled');
|
||||
expect(darkMode).toBe('auto');
|
||||
});
|
||||
|
||||
it('should handle multi-environment deployment configuration', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.APP_VERSION = '1.2.3';
|
||||
process.env.BUILD_NUMBER = '456';
|
||||
process.env.DEPLOY_REGION = 'us-west-2';
|
||||
|
||||
expect(() => validateEnv(['NODE_ENV'])).not.toThrow();
|
||||
|
||||
const env = getRequiredEnv('NODE_ENV');
|
||||
const version = getOptionalEnv('APP_VERSION', '0.0.0');
|
||||
const buildNumber = getOptionalEnv('BUILD_NUMBER', 'local');
|
||||
const region = getOptionalEnv('DEPLOY_REGION', 'us-east-1');
|
||||
|
||||
expect(env).toBe('production');
|
||||
expect(version).toBe('1.2.3');
|
||||
expect(buildNumber).toBe('456');
|
||||
expect(region).toBe('us-west-2');
|
||||
});
|
||||
});
|
||||
});
|
||||
393
src/stores/workspace/rightSidePanelStore.test.ts
Normal file
393
src/stores/workspace/rightSidePanelStore.test.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useRightSidePanelStore } from './rightSidePanelStore'
|
||||
|
||||
describe('rightSidePanelStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('initializes with default values', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
expect(store.activeTab).toBe('parameters')
|
||||
})
|
||||
|
||||
it('creates a new instance for each pinia context', () => {
|
||||
const store1 = useRightSidePanelStore()
|
||||
const pinia2 = createPinia()
|
||||
setActivePinia(pinia2)
|
||||
const store2 = useRightSidePanelStore()
|
||||
|
||||
store1.isOpen = true
|
||||
expect(store2.isOpen).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('openPanel', () => {
|
||||
it('opens panel and sets active tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('settings')
|
||||
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('settings')
|
||||
})
|
||||
|
||||
it('opens panel with parameters tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('parameters')
|
||||
})
|
||||
|
||||
it('opens panel with info tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('info')
|
||||
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('info')
|
||||
})
|
||||
|
||||
it('can switch tabs while panel is already open', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
expect(store.activeTab).toBe('parameters')
|
||||
|
||||
store.openPanel('settings')
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('settings')
|
||||
})
|
||||
|
||||
it('overwrites previous active tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
store.openPanel('info')
|
||||
|
||||
expect(store.activeTab).toBe('info')
|
||||
expect(store.activeTab).not.toBe('parameters')
|
||||
})
|
||||
})
|
||||
|
||||
describe('closePanel', () => {
|
||||
it('closes an open panel', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
expect(store.isOpen).toBe(true)
|
||||
|
||||
store.closePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('maintains active tab when closing', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('settings')
|
||||
const tabBeforeClose = store.activeTab
|
||||
|
||||
store.closePanel()
|
||||
|
||||
expect(store.activeTab).toBe(tabBeforeClose)
|
||||
})
|
||||
|
||||
it('is idempotent when called multiple times', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
store.closePanel()
|
||||
store.closePanel()
|
||||
store.closePanel()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('works correctly when panel is already closed', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
store.closePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('togglePanel', () => {
|
||||
it('opens closed panel', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('closes open panel', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
expect(store.isOpen).toBe(true)
|
||||
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('alternates state on repeated calls', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(true)
|
||||
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves active tab when toggling', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('settings')
|
||||
const originalTab = store.activeTab
|
||||
|
||||
store.togglePanel()
|
||||
store.togglePanel()
|
||||
|
||||
expect(store.activeTab).toBe(originalTab)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Active Tab Management', () => {
|
||||
it('defaults to parameters tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
expect(store.activeTab).toBe('parameters')
|
||||
})
|
||||
|
||||
it('allows all valid tab types', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
const validTabs: Array<'parameters' | 'settings' | 'info'> = [
|
||||
'parameters',
|
||||
'settings',
|
||||
'info'
|
||||
]
|
||||
|
||||
validTabs.forEach(tab => {
|
||||
store.openPanel(tab)
|
||||
expect(store.activeTab).toBe(tab)
|
||||
})
|
||||
})
|
||||
|
||||
it('can be updated directly', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.activeTab = 'settings'
|
||||
expect(store.activeTab).toBe('settings')
|
||||
|
||||
store.activeTab = 'info'
|
||||
expect(store.activeTab).toBe('info')
|
||||
})
|
||||
})
|
||||
|
||||
describe('State Persistence', () => {
|
||||
it('maintains state across multiple operations', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('parameters')
|
||||
store.closePanel()
|
||||
store.openPanel('settings')
|
||||
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('settings')
|
||||
})
|
||||
|
||||
it('independent operations do not interfere', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// Set active tab directly
|
||||
store.activeTab = 'info'
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
// Toggle panel
|
||||
store.togglePanel()
|
||||
expect(store.activeTab).toBe('info')
|
||||
expect(store.isOpen).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles rapid toggle operations', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
store.togglePanel()
|
||||
}
|
||||
|
||||
// Should be closed (even number of toggles)
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('handles rapid tab switching', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
const tabs: Array<'parameters' | 'settings' | 'info'> = [
|
||||
'parameters',
|
||||
'settings',
|
||||
'info'
|
||||
]
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const tab = tabs[i % tabs.length]
|
||||
store.openPanel(tab)
|
||||
}
|
||||
|
||||
// Should end on info tab (50 % 3 = 2, which is 'info')
|
||||
expect(store.activeTab).toBe('info')
|
||||
expect(store.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('maintains type safety', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// TypeScript should enforce correct types
|
||||
store.openPanel('parameters')
|
||||
store.openPanel('settings')
|
||||
store.openPanel('info')
|
||||
|
||||
expect(['parameters', 'settings', 'info']).toContain(store.activeTab)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Workflow Scenarios', () => {
|
||||
it('supports typical user workflow: open, switch tabs, close', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// User opens parameters
|
||||
store.openPanel('parameters')
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('parameters')
|
||||
|
||||
// User switches to settings
|
||||
store.openPanel('settings')
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('settings')
|
||||
|
||||
// User closes panel
|
||||
store.closePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('supports quick toggle workflow', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// User toggles panel (opens with default tab)
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(true)
|
||||
|
||||
// User quickly toggles to close
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('supports switching to info tab from external action', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// Panel might be closed
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
// User clicks "Info" button which opens panel to info tab
|
||||
store.openPanel('info')
|
||||
expect(store.isOpen).toBe(true)
|
||||
expect(store.activeTab).toBe('info')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration with UI', () => {
|
||||
it('supports binding to panel visibility', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// Component v-if="store.isOpen"
|
||||
expect(store.isOpen).toBe(false)
|
||||
|
||||
store.openPanel('parameters')
|
||||
expect(store.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('supports binding to active tab', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
store.openPanel('settings')
|
||||
|
||||
// Component v-if="store.activeTab === 'settings'"
|
||||
expect(store.activeTab).toBe('settings')
|
||||
})
|
||||
|
||||
it('supports toggle button binding', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// Button @click="store.togglePanel"
|
||||
const initialState = store.isOpen
|
||||
store.togglePanel()
|
||||
expect(store.isOpen).toBe(!initialState)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Reactive Properties', () => {
|
||||
it('isOpen is reactive', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
const values: boolean[] = []
|
||||
|
||||
// Simulating a watcher
|
||||
const stopWatch = vi.fn(() => {
|
||||
values.push(store.isOpen)
|
||||
})
|
||||
|
||||
stopWatch()
|
||||
store.togglePanel()
|
||||
stopWatch()
|
||||
store.togglePanel()
|
||||
stopWatch()
|
||||
|
||||
expect(values).toEqual([false, true, false])
|
||||
})
|
||||
|
||||
it('activeTab is reactive', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
const tabs: string[] = []
|
||||
|
||||
const recordTab = () => tabs.push(store.activeTab)
|
||||
|
||||
recordTab()
|
||||
store.openPanel('settings')
|
||||
recordTab()
|
||||
store.openPanel('info')
|
||||
recordTab()
|
||||
|
||||
expect(tabs).toEqual(['parameters', 'settings', 'info'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Type Safety', () => {
|
||||
it('enforces correct tab types at runtime', () => {
|
||||
const store = useRightSidePanelStore()
|
||||
|
||||
// These should work
|
||||
store.openPanel('parameters')
|
||||
store.openPanel('settings')
|
||||
store.openPanel('info')
|
||||
|
||||
// TypeScript should prevent invalid tabs at compile time
|
||||
// @ts-expect-error - testing invalid tab
|
||||
// store.openPanel('invalid')
|
||||
|
||||
expect(store.activeTab).toBe('info')
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user