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: ` +
+
+ Nested Content +
+
+ ` + } + ) + + 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