diff --git a/src/components/rightSidePanel/info/TabInfo.test.ts b/src/components/rightSidePanel/info/TabInfo.test.ts
new file mode 100644
index 0000000000..05e900baba
--- /dev/null
+++ b/src/components/rightSidePanel/info/TabInfo.test.ts
@@ -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: '
{{ node?.type }}
',
+ 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('')
+ })
+ })
+
+ 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('')
+ })
+
+ 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('')
+ })
+
+ 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('')
+ })
+
+ 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']
+ )
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/components/rightSidePanel/layout/RightPanelSection.test.ts b/src/components/rightSidePanel/layout/RightPanelSection.test.ts
new file mode 100644
index 0000000000..3d3b6f0c13
--- /dev/null
+++ b/src/components/rightSidePanel/layout/RightPanelSection.test.ts
@@ -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: 'Slot Label'
+ }
+ )
+ 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: 'Test Content
'
+ }
+ )
+
+ // 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: 'Content
'
+ }
+ )
+
+ expect(wrapper.html()).toContain('content')
+ })
+
+ it('starts in collapsed state when defaultCollapse is true', async () => {
+ const wrapper = mountComponent(
+ { defaultCollapse: true },
+ {
+ default: 'Content
'
+ }
+ )
+
+ await nextTick()
+ expect(wrapper.html()).not.toContain('content')
+ })
+
+ it('toggles collapse state when button is clicked', async () => {
+ const wrapper = mountComponent(
+ {},
+ {
+ default: 'Content
'
+ }
+ )
+
+ 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: 'Content
'
+ }
+ )
+
+ // 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: 'Content
'
+ }
+ )
+
+ // 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: 'Content
'
+ }
+ )
+ 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: 'Content 1
' }
+ )
+ const wrapper2 = mountComponent(
+ { label: 'Section 2' },
+ { default: 'Content 2
' }
+ )
+
+ // 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: `
+
+ `
+ }
+ )
+
+ 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: `
+ Child 1
+ Child 2
+ Child 3
+ `
+ }
+ )
+
+ 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: 'Styled
'
+ }
+ )
+
+ 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) =>
+ `Item ${i}
`
+ ).join('')
+
+ const wrapper = mountComponent(
+ {},
+ { default: largeContent }
+ )
+
+ expect(wrapper.exists()).toBe(true)
+ expect(wrapper.html()).toContain('item-0')
+ expect(wrapper.html()).toContain('item-99')
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/index.test.ts b/src/index.test.ts
new file mode 100644
index 0000000000..77c55ca4f7
--- /dev/null
+++ b/src/index.test.ts
@@ -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');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/stores/workspace/rightSidePanelStore.test.ts b/src/stores/workspace/rightSidePanelStore.test.ts
new file mode 100644
index 0000000000..4aa20dff98
--- /dev/null
+++ b/src/stores/workspace/rightSidePanelStore.test.ts
@@ -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')
+ })
+ })
+})
\ No newline at end of file