CodeRabbit Generated Unit Tests: Add comprehensive unit tests for right panel components and stores

This commit is contained in:
coderabbitai[bot]
2025-12-06 05:45:14 +00:00
committed by GitHub
parent a05d34c704
commit 364e662bc3
4 changed files with 2096 additions and 0 deletions

View File

@@ -0,0 +1,426 @@
import { mount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import PrimeVue from 'primevue/config'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import NodeHelpContent from '@/components/node/NodeHelpContent.vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
import TabInfo from './TabInfo.vue'
// Mock the stores
vi.mock('@/stores/nodeDefStore', () => ({
useNodeDefStore: vi.fn()
}))
vi.mock('@/stores/workspace/nodeHelpStore', () => ({
useNodeHelpStore: vi.fn()
}))
// Mock NodeHelpContent component
vi.mock('@/components/node/NodeHelpContent.vue', () => ({
default: {
name: 'NodeHelpContent',
template: '<div class="node-help-content">{{ node?.type }}</div>',
props: ['node']
}
}))
describe('TabInfo', () => {
let mockNodeDefStore: any
let mockNodeHelpStore: any
beforeEach(() => {
setActivePinia(createPinia())
mockNodeDefStore = {
nodeDefsByName: {
'KSampler': {
name: 'KSampler',
display_name: 'KSampler',
description: 'Sampling node',
category: 'sampling'
},
'CheckpointLoader': {
name: 'CheckpointLoader',
display_name: 'Load Checkpoint',
description: 'Loads model checkpoint',
category: 'loaders'
}
}
}
mockNodeHelpStore = {
openHelp: vi.fn()
}
vi.mocked(useNodeDefStore).mockReturnValue(mockNodeDefStore)
vi.mocked(useNodeHelpStore).mockReturnValue(mockNodeHelpStore)
})
const createMockNode = (type: string, id: number = 1): LGraphNode => ({
id,
type,
title: `${type}_${id}`,
properties: {},
serialize: vi.fn(),
configure: vi.fn()
} as any)
const mountComponent = (props = {}) => {
return mount(TabInfo, {
global: {
plugins: [PrimeVue],
stubs: {
NodeHelpContent: true
}
},
props: {
nodes: [createMockNode('KSampler')],
...props
}
})
}
describe('Rendering', () => {
it('renders successfully with single node', () => {
const wrapper = mountComponent()
expect(wrapper.exists()).toBe(true)
})
it('renders NodeHelpContent when node info exists', () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.exists()).toBe(true)
})
it('renders with correct container styling', () => {
const wrapper = mountComponent()
const container = wrapper.find('div')
expect(container.classes()).toContain('rounded-lg')
expect(container.classes()).toContain('bg-interface-surface')
expect(container.classes()).toContain('p-3')
})
it('does not render when node info is not available', () => {
mockNodeDefStore.nodeDefsByName = {}
const wrapper = mountComponent({
nodes: [createMockNode('UnknownNode')]
})
expect(wrapper.html()).toBe('<!--v-if-->')
})
})
describe('Node Info Computation', () => {
it('computes node info from first node in array', () => {
const nodes = [
createMockNode('KSampler', 1),
createMockNode('CheckpointLoader', 2)
]
const wrapper = mountComponent({ nodes })
// Should use first node
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.exists()).toBe(true)
})
it('returns node definition for valid node type', () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node')).toEqual(
mockNodeDefStore.nodeDefsByName['KSampler']
)
})
it('returns undefined for invalid node type', () => {
mockNodeDefStore.nodeDefsByName = {}
const wrapper = mountComponent({
nodes: [createMockNode('InvalidNode')]
})
expect(wrapper.html()).toBe('<!--v-if-->')
})
it('handles nodes array with single node', () => {
const wrapper = mountComponent({
nodes: [createMockNode('CheckpointLoader')]
})
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node')).toEqual(
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
)
})
})
describe('Help Store Integration', () => {
it('calls openHelp when nodeInfo exists', async () => {
mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalled()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
mockNodeDefStore.nodeDefsByName['KSampler']
)
})
it('opens help immediately on mount', async () => {
mockNodeHelpStore.openHelp.mockClear()
mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledTimes(1)
})
it('updates help when node changes', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
mockNodeHelpStore.openHelp.mockClear()
// Change to different node
await wrapper.setProps({
nodes: [createMockNode('CheckpointLoader')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
)
})
it('does not call openHelp when nodeInfo is undefined', async () => {
mockNodeDefStore.nodeDefsByName = {}
mockNodeHelpStore.openHelp.mockClear()
mountComponent({
nodes: [createMockNode('UnknownNode')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).not.toHaveBeenCalled()
})
})
describe('Props Handling', () => {
it('accepts nodes prop as array', () => {
const nodes = [
createMockNode('KSampler', 1),
createMockNode('CheckpointLoader', 2)
]
const wrapper = mountComponent({ nodes })
expect(wrapper.props('nodes')).toEqual(nodes)
})
it('handles empty nodes array', () => {
const wrapper = mountComponent({ nodes: [] })
expect(wrapper.html()).toBe('<!--v-if-->')
})
it('updates when nodes prop changes', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
await wrapper.setProps({
nodes: [createMockNode('CheckpointLoader')]
})
await nextTick()
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node')).toEqual(
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
)
})
})
describe('Edge Cases', () => {
it('handles node with missing type', () => {
const nodeWithoutType = {
id: 1,
title: 'Node',
properties: {}
} as any
const wrapper = mountComponent({
nodes: [nodeWithoutType]
})
expect(wrapper.html()).toBe('<!--v-if-->')
})
it('handles rapid node switching', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
for (let i = 0; i < 10; i++) {
await wrapper.setProps({
nodes: [createMockNode(i % 2 === 0 ? 'KSampler' : 'CheckpointLoader')]
})
}
await nextTick()
// Should still be functional
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.exists()).toBe(true)
})
it('handles nodes with special characters in type', () => {
mockNodeDefStore.nodeDefsByName['Node-With-Dashes'] = {
name: 'Node-With-Dashes',
display_name: 'Node With Dashes'
}
const wrapper = mountComponent({
nodes: [createMockNode('Node-With-Dashes')]
})
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.exists()).toBe(true)
})
})
describe('Reactivity', () => {
it('recomputes nodeInfo when nodes prop changes', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
let helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node').name).toBe('KSampler')
await wrapper.setProps({
nodes: [createMockNode('CheckpointLoader')]
})
helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node').name).toBe('CheckpointLoader')
})
it('recomputes nodeInfo when store updates', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
// Simulate store update
mockNodeDefStore.nodeDefsByName['KSampler'] = {
...mockNodeDefStore.nodeDefsByName['KSampler'],
description: 'Updated description'
}
await nextTick()
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.props('node').description).toBe('Updated description')
})
})
describe('Component Lifecycle', () => {
it('calls openHelp on mount', async () => {
mockNodeHelpStore.openHelp.mockClear()
mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledTimes(1)
})
it('cleans up watchers on unmount', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
wrapper.unmount()
// Should not throw errors
expect(true).toBe(true)
})
it('handles remount correctly', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
wrapper.unmount()
mockNodeHelpStore.openHelp.mockClear()
const wrapper2 = mountComponent({
nodes: [createMockNode('CheckpointLoader')]
})
await nextTick()
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
)
})
})
describe('Integration Scenarios', () => {
it('works in typical workflow: select node, view info', async () => {
// User selects a node
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
// Info panel opens and displays help
expect(mockNodeHelpStore.openHelp).toHaveBeenCalled()
const helpContent = wrapper.findComponent({ name: 'NodeHelpContent' })
expect(helpContent.exists()).toBe(true)
})
it('updates when user selects different node', async () => {
const wrapper = mountComponent({
nodes: [createMockNode('KSampler')]
})
await nextTick()
mockNodeHelpStore.openHelp.mockClear()
// User selects different node
await wrapper.setProps({
nodes: [createMockNode('CheckpointLoader')]
})
await nextTick()
// Help updates to new node
expect(mockNodeHelpStore.openHelp).toHaveBeenCalledWith(
mockNodeDefStore.nodeDefsByName['CheckpointLoader']
)
})
})
})

View File

@@ -0,0 +1,408 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import { beforeAll, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import RightPanelSection from './RightPanelSection.vue'
describe('RightPanelSection', () => {
beforeAll(() => {
const app = { use: vi.fn() }
app.use(PrimeVue)
})
const mountComponent = (props = {}, slots = {}) => {
return mount(RightPanelSection, {
global: {
plugins: [PrimeVue]
},
props: {
label: 'Test Section',
...props
},
slots
})
}
describe('Rendering', () => {
it('renders with default props', () => {
const wrapper = mountComponent()
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('button').exists()).toBe(true)
})
it('displays label from props', () => {
const wrapper = mountComponent({ label: 'Custom Label' })
const button = wrapper.find('button')
expect(button.text()).toContain('Custom Label')
})
it('renders label from slot when provided', () => {
const wrapper = mountComponent(
{},
{
label: '<span class="custom-label">Slot Label</span>'
}
)
const button = wrapper.find('button')
expect(button.html()).toContain('custom-label')
expect(button.text()).toContain('Slot Label')
})
it('renders default slot content when expanded', async () => {
const wrapper = mountComponent(
{},
{
default: '<div class="test-content">Test Content</div>'
}
)
// Content should be visible by default (not collapsed)
expect(wrapper.html()).toContain('test-content')
expect(wrapper.text()).toContain('Test Content')
})
it('renders chevron icon in button', () => {
const wrapper = mountComponent()
const icon = wrapper.find('i.icon-\\[lucide--chevron-down\\]')
expect(icon.exists()).toBe(true)
})
})
describe('Collapse/Expand Functionality', () => {
it('starts in expanded state by default', () => {
const wrapper = mountComponent(
{},
{
default: '<div class="content">Content</div>'
}
)
expect(wrapper.html()).toContain('content')
})
it('starts in collapsed state when defaultCollapse is true', async () => {
const wrapper = mountComponent(
{ defaultCollapse: true },
{
default: '<div class="content">Content</div>'
}
)
await nextTick()
expect(wrapper.html()).not.toContain('content')
})
it('toggles collapse state when button is clicked', async () => {
const wrapper = mountComponent(
{},
{
default: '<div class="content">Content</div>'
}
)
const button = wrapper.find('button')
// Initially expanded
expect(wrapper.html()).toContain('content')
// Click to collapse
await button.trigger('click')
await nextTick()
expect(wrapper.html()).not.toContain('content')
// Click to expand again
await button.trigger('click')
await nextTick()
expect(wrapper.html()).toContain('content')
})
it('rotates chevron icon when collapsed', async () => {
const wrapper = mountComponent()
const button = wrapper.find('button')
const icon = wrapper.find('i.icon-\\[lucide--chevron-down\\]')
// Initially not rotated (expanded state)
expect(icon.classes()).not.toContain('rotate-90')
// Click to collapse
await button.trigger('click')
await nextTick()
// Should be rotated when collapsed
expect(icon.classes()).toContain('rotate-90')
})
it('emits update:collapse event when toggled', async () => {
const wrapper = mountComponent()
const button = wrapper.find('button')
await button.trigger('click')
await nextTick()
expect(wrapper.emitted('update:collapse')).toBeTruthy()
expect(wrapper.emitted('update:collapse')?.[0]).toEqual([true])
})
it('supports v-model:collapse binding', async () => {
const wrapper = mountComponent({ collapse: false })
const button = wrapper.find('button')
await button.trigger('click')
await nextTick()
expect(wrapper.emitted('update:collapse')?.[0]).toEqual([true])
})
})
describe('Reactivity', () => {
it('reacts to defaultCollapse prop changes', async () => {
const wrapper = mountComponent(
{ defaultCollapse: false },
{
default: '<div class="content">Content</div>'
}
)
// Initially expanded
expect(wrapper.html()).toContain('content')
// Change to collapsed
await wrapper.setProps({ defaultCollapse: true })
await nextTick()
expect(wrapper.html()).not.toContain('content')
})
it('updates when collapse model value changes', async () => {
const wrapper = mountComponent(
{ collapse: false },
{
default: '<div class="content">Content</div>'
}
)
// Initially expanded
expect(wrapper.html()).toContain('content')
// Programmatically collapse
await wrapper.setProps({ collapse: true })
await nextTick()
expect(wrapper.html()).not.toContain('content')
})
})
describe('Styling and Classes', () => {
it('applies correct sticky header styles', () => {
const wrapper = mountComponent()
const header = wrapper.find('.sticky')
expect(header.exists()).toBe(true)
expect(header.classes()).toContain('top-0')
expect(header.classes()).toContain('z-10')
})
it('applies button classes correctly', () => {
const wrapper = mountComponent()
const button = wrapper.find('button')
expect(button.classes()).toContain('group')
expect(button.classes()).toContain('cursor-pointer')
expect(button.classes()).toContain('w-full')
})
it('applies transition class to chevron icon', () => {
const wrapper = mountComponent()
const icon = wrapper.find('i')
expect(icon.classes()).toContain('transition-all')
})
it('applies hover styles to icon through group', () => {
const wrapper = mountComponent()
const icon = wrapper.find('i')
expect(icon.classes()).toContain('group-hover:text-base-foreground')
})
})
describe('Accessibility', () => {
it('button is keyboard accessible', () => {
const wrapper = mountComponent()
const button = wrapper.find('button')
expect(button.element.tagName).toBe('BUTTON')
})
it('maintains semantic HTML structure', () => {
const wrapper = mountComponent()
// Should have a div container
expect(wrapper.element.tagName).toBe('DIV')
// Should have a button for interaction
expect(wrapper.find('button').exists()).toBe(true)
})
it('label text is accessible', () => {
const wrapper = mountComponent({ label: 'Test Section' })
const button = wrapper.find('button')
expect(button.text()).toContain('Test Section')
})
})
describe('Edge Cases', () => {
it('handles empty label gracefully', () => {
const wrapper = mountComponent({ label: '' })
const button = wrapper.find('button')
expect(button.exists()).toBe(true)
expect(button.text().trim()).toBe('')
})
it('handles undefined label', () => {
const wrapper = mountComponent({ label: undefined })
expect(wrapper.find('button').exists()).toBe(true)
})
it('handles null default slot', () => {
const wrapper = mountComponent()
expect(wrapper.exists()).toBe(true)
})
it('handles multiple rapid toggle clicks', async () => {
const wrapper = mountComponent(
{},
{
default: '<div class="content">Content</div>'
}
)
const button = wrapper.find('button')
// Rapidly click multiple times
await button.trigger('click')
await button.trigger('click')
await button.trigger('click')
await nextTick()
// Should handle all clicks without errors
expect(wrapper.emitted('update:collapse')).toHaveLength(3)
})
it('handles very long label text', () => {
const longLabel = 'A'.repeat(1000)
const wrapper = mountComponent({ label: longLabel })
const span = wrapper.find('span.line-clamp-2')
expect(span.exists()).toBe(true)
expect(span.text()).toContain('A')
})
})
describe('Integration Scenarios', () => {
it('works with multiple sections in sequence', () => {
const wrapper1 = mountComponent({ label: 'Section 1' })
const wrapper2 = mountComponent({ label: 'Section 2' })
expect(wrapper1.text()).toContain('Section 1')
expect(wrapper2.text()).toContain('Section 2')
})
it('maintains independent state between instances', async () => {
const wrapper1 = mountComponent(
{ label: 'Section 1' },
{ default: '<div>Content 1</div>' }
)
const wrapper2 = mountComponent(
{ label: 'Section 2' },
{ default: '<div>Content 2</div>' }
)
// Collapse first section
await wrapper1.find('button').trigger('click')
await nextTick()
// First should be collapsed
expect(wrapper1.html()).not.toContain('Content 1')
// Second should remain expanded
expect(wrapper2.html()).toContain('Content 2')
})
})
describe('Content Slot Behavior', () => {
it('renders complex nested content', () => {
const wrapper = mountComponent(
{},
{
default: `
<div class="outer">
<div class="inner">
<span class="text">Nested Content</span>
</div>
</div>
`
}
)
expect(wrapper.html()).toContain('outer')
expect(wrapper.html()).toContain('inner')
expect(wrapper.text()).toContain('Nested Content')
})
it('renders multiple child elements', () => {
const wrapper = mountComponent(
{},
{
default: `
<div class="child-1">Child 1</div>
<div class="child-2">Child 2</div>
<div class="child-3">Child 3</div>
`
}
)
expect(wrapper.html()).toContain('child-1')
expect(wrapper.html()).toContain('child-2')
expect(wrapper.html()).toContain('child-3')
})
it('preserves slot content styling', () => {
const wrapper = mountComponent(
{},
{
default: '<div style="color: red;" class="styled-content">Styled</div>'
}
)
expect(wrapper.html()).toContain('styled-content')
expect(wrapper.html()).toContain('color: red')
})
})
describe('Performance', () => {
it('handles rapid mount/unmount cycles', () => {
for (let i = 0; i < 10; i++) {
const wrapper = mountComponent()
expect(wrapper.exists()).toBe(true)
wrapper.unmount()
}
})
it('efficiently handles large content in slot', () => {
const largeContent = Array.from({ length: 100 }, (_, i) =>
`<div class="item-${i}">Item ${i}</div>`
).join('')
const wrapper = mountComponent(
{},
{ default: largeContent }
)
expect(wrapper.exists()).toBe(true)
expect(wrapper.html()).toContain('item-0')
expect(wrapper.html()).toContain('item-99')
})
})
})

869
src/index.test.ts Normal file
View File

@@ -0,0 +1,869 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { getRequiredEnv, getOptionalEnv, validateEnv } from './index';
describe('Environment Variable Utilities', () => {
// Store original env to restore after tests
const originalEnv = { ...process.env };
beforeEach(() => {
// Clear environment before each test
process.env = { ...originalEnv };
});
afterEach(() => {
// Restore original environment after each test
process.env = originalEnv;
vi.restoreAllMocks();
});
describe('getRequiredEnv', () => {
describe('happy path', () => {
it('should return the value when environment variable exists', () => {
process.env.TEST_VAR = 'test-value';
const result = getRequiredEnv('TEST_VAR');
expect(result).toBe('test-value');
});
it('should return value with special characters', () => {
process.env.SPECIAL_VAR = 'value-with-@#$%^&*()_+={}[]|:;"<>?,./~`';
const result = getRequiredEnv('SPECIAL_VAR');
expect(result).toBe('value-with-@#$%^&*()_+={}[]|:;"<>?,./~`');
});
it('should return value with whitespace', () => {
process.env.WHITESPACE_VAR = ' value with spaces ';
const result = getRequiredEnv('WHITESPACE_VAR');
expect(result).toBe(' value with spaces ');
});
it('should return multiline value', () => {
process.env.MULTILINE_VAR = 'line1\nline2\nline3';
const result = getRequiredEnv('MULTILINE_VAR');
expect(result).toBe('line1\nline2\nline3');
});
it('should return very long value', () => {
const longValue = 'a'.repeat(10000);
process.env.LONG_VAR = longValue;
const result = getRequiredEnv('LONG_VAR');
expect(result).toBe(longValue);
});
it('should return numeric string value', () => {
process.env.NUMERIC_VAR = '12345';
const result = getRequiredEnv('NUMERIC_VAR');
expect(result).toBe('12345');
});
it('should return boolean string value', () => {
process.env.BOOL_VAR = 'true';
const result = getRequiredEnv('BOOL_VAR');
expect(result).toBe('true');
});
it('should handle unicode characters', () => {
process.env.UNICODE_VAR = '你好世界🌍🚀';
const result = getRequiredEnv('UNICODE_VAR');
expect(result).toBe('你好世界🌍🚀');
});
it('should handle URL values', () => {
process.env.URL_VAR = 'https://example.com:8080/path?query=value&another=test#fragment';
const result = getRequiredEnv('URL_VAR');
expect(result).toBe('https://example.com:8080/path?query=value&another=test#fragment');
});
it('should handle JSON string values', () => {
const jsonValue = '{"key":"value","nested":{"array":[1,2,3]}}';
process.env.JSON_VAR = jsonValue;
const result = getRequiredEnv('JSON_VAR');
expect(result).toBe(jsonValue);
});
});
describe('edge cases', () => {
it('should throw error when variable is undefined', () => {
delete process.env.UNDEFINED_VAR;
expect(() => getRequiredEnv('UNDEFINED_VAR')).toThrow();
});
it('should throw error with descriptive message for missing variable', () => {
delete process.env.MISSING_VAR;
expect(() => getRequiredEnv('MISSING_VAR')).toThrow('Environment variable MISSING_VAR is required but not set');
});
it('should throw error when variable is empty string', () => {
process.env.EMPTY_VAR = '';
expect(() => getRequiredEnv('EMPTY_VAR')).toThrow('Environment variable EMPTY_VAR is required but not set');
});
it('should throw error when variable is only whitespace', () => {
process.env.WHITESPACE_ONLY_VAR = ' ';
expect(() => getRequiredEnv('WHITESPACE_ONLY_VAR')).toThrow();
});
it('should handle variable name with special characters', () => {
process.env['VAR_WITH-DASH'] = 'value';
const result = getRequiredEnv('VAR_WITH-DASH');
expect(result).toBe('value');
});
it('should handle variable name with numbers', () => {
process.env.VAR123 = 'value';
const result = getRequiredEnv('VAR123');
expect(result).toBe('value');
});
it('should handle single character variable name', () => {
process.env.X = 'value';
const result = getRequiredEnv('X');
expect(result).toBe('value');
});
it('should handle very long variable name', () => {
const longName = 'VAR_' + 'A'.repeat(200);
process.env[longName] = 'value';
const result = getRequiredEnv(longName);
expect(result).toBe('value');
});
it('should return "0" without throwing (zero is a valid value)', () => {
process.env.ZERO_VAR = '0';
const result = getRequiredEnv('ZERO_VAR');
expect(result).toBe('0');
});
it('should return "false" without throwing (false string is valid)', () => {
process.env.FALSE_VAR = 'false';
const result = getRequiredEnv('FALSE_VAR');
expect(result).toBe('false');
});
});
describe('error conditions', () => {
it('should throw TypeError when varName is not a string', () => {
expect(() => getRequiredEnv(null as any)).toThrow();
});
it('should throw TypeError when varName is undefined', () => {
expect(() => getRequiredEnv(undefined as any)).toThrow();
});
it('should throw TypeError when varName is a number', () => {
expect(() => getRequiredEnv(123 as any)).toThrow();
});
it('should throw TypeError when varName is an object', () => {
expect(() => getRequiredEnv({} as any)).toThrow();
});
it('should throw TypeError when varName is an array', () => {
expect(() => getRequiredEnv([] as any)).toThrow();
});
it('should throw when varName is empty string', () => {
expect(() => getRequiredEnv('')).toThrow();
});
});
describe('integration scenarios', () => {
it('should work correctly when called multiple times for same variable', () => {
process.env.MULTI_CALL_VAR = 'value';
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
expect(getRequiredEnv('MULTI_CALL_VAR')).toBe('value');
});
it('should work correctly when called for different variables', () => {
process.env.VAR_A = 'value-a';
process.env.VAR_B = 'value-b';
process.env.VAR_C = 'value-c';
expect(getRequiredEnv('VAR_A')).toBe('value-a');
expect(getRequiredEnv('VAR_B')).toBe('value-b');
expect(getRequiredEnv('VAR_C')).toBe('value-c');
});
it('should reflect environment changes between calls', () => {
process.env.DYNAMIC_VAR = 'initial';
expect(getRequiredEnv('DYNAMIC_VAR')).toBe('initial');
process.env.DYNAMIC_VAR = 'updated';
expect(getRequiredEnv('DYNAMIC_VAR')).toBe('updated');
});
});
});
describe('getOptionalEnv', () => {
describe('happy path', () => {
it('should return the value when environment variable exists', () => {
process.env.OPTIONAL_VAR = 'optional-value';
const result = getOptionalEnv('OPTIONAL_VAR');
expect(result).toBe('optional-value');
});
it('should return undefined when variable does not exist', () => {
delete process.env.NONEXISTENT_VAR;
const result = getOptionalEnv('NONEXISTENT_VAR');
expect(result).toBeUndefined();
});
it('should return default value when variable does not exist', () => {
delete process.env.DEFAULT_VAR;
const result = getOptionalEnv('DEFAULT_VAR', 'default-value');
expect(result).toBe('default-value');
});
it('should return actual value over default when variable exists', () => {
process.env.PRIORITY_VAR = 'actual-value';
const result = getOptionalEnv('PRIORITY_VAR', 'default-value');
expect(result).toBe('actual-value');
});
it('should handle special characters in value', () => {
process.env.SPECIAL_OPTIONAL = '@#$%^&*()_+{}[]|:;"<>?,./';
const result = getOptionalEnv('SPECIAL_OPTIONAL');
expect(result).toBe('@#$%^&*()_+{}[]|:;"<>?,./');
});
it('should handle whitespace in value', () => {
process.env.WHITESPACE_OPTIONAL = ' spaced value ';
const result = getOptionalEnv('WHITESPACE_OPTIONAL');
expect(result).toBe(' spaced value ');
});
it('should handle multiline value', () => {
process.env.MULTILINE_OPTIONAL = 'line1\nline2\nline3';
const result = getOptionalEnv('MULTILINE_OPTIONAL');
expect(result).toBe('line1\nline2\nline3');
});
it('should handle unicode characters', () => {
process.env.UNICODE_OPTIONAL = '测试🎉';
const result = getOptionalEnv('UNICODE_OPTIONAL');
expect(result).toBe('测试🎉');
});
it('should handle URL values', () => {
process.env.URL_OPTIONAL = 'https://api.example.com/v1/endpoint';
const result = getOptionalEnv('URL_OPTIONAL');
expect(result).toBe('https://api.example.com/v1/endpoint');
});
it('should return numeric string', () => {
process.env.NUM_OPTIONAL = '42';
const result = getOptionalEnv('NUM_OPTIONAL');
expect(result).toBe('42');
});
it('should return "0" as valid value', () => {
process.env.ZERO_OPTIONAL = '0';
const result = getOptionalEnv('ZERO_OPTIONAL');
expect(result).toBe('0');
});
it('should return "false" as valid value', () => {
process.env.FALSE_OPTIONAL = 'false';
const result = getOptionalEnv('FALSE_OPTIONAL');
expect(result).toBe('false');
});
});
describe('edge cases', () => {
it('should return undefined for empty string when no default provided', () => {
process.env.EMPTY_OPTIONAL = '';
const result = getOptionalEnv('EMPTY_OPTIONAL');
expect(result).toBeUndefined();
});
it('should return default for empty string when default provided', () => {
process.env.EMPTY_WITH_DEFAULT = '';
const result = getOptionalEnv('EMPTY_WITH_DEFAULT', 'fallback');
expect(result).toBe('fallback');
});
it('should return undefined for whitespace-only value', () => {
process.env.WHITESPACE_ONLY_OPTIONAL = ' ';
const result = getOptionalEnv('WHITESPACE_ONLY_OPTIONAL');
expect(result).toBeUndefined();
});
it('should handle null as default value', () => {
delete process.env.NULL_DEFAULT_VAR;
const result = getOptionalEnv('NULL_DEFAULT_VAR', null as any);
expect(result).toBeNull();
});
it('should handle empty string as default value', () => {
delete process.env.EMPTY_DEFAULT_VAR;
const result = getOptionalEnv('EMPTY_DEFAULT_VAR', '');
expect(result).toBe('');
});
it('should handle numeric default value', () => {
delete process.env.NUMERIC_DEFAULT_VAR;
const result = getOptionalEnv('NUMERIC_DEFAULT_VAR', 123 as any);
expect(result).toBe(123);
});
it('should handle boolean default value', () => {
delete process.env.BOOLEAN_DEFAULT_VAR;
const result = getOptionalEnv('BOOLEAN_DEFAULT_VAR', true as any);
expect(result).toBe(true);
});
it('should handle object as default value', () => {
delete process.env.OBJECT_DEFAULT_VAR;
const defaultObj = { key: 'value' };
const result = getOptionalEnv('OBJECT_DEFAULT_VAR', defaultObj as any);
expect(result).toBe(defaultObj);
});
it('should handle array as default value', () => {
delete process.env.ARRAY_DEFAULT_VAR;
const defaultArray = [1, 2, 3];
const result = getOptionalEnv('ARRAY_DEFAULT_VAR', defaultArray as any);
expect(result).toBe(defaultArray);
});
it('should handle very long default value', () => {
delete process.env.LONG_DEFAULT_VAR;
const longDefault = 'x'.repeat(10000);
const result = getOptionalEnv('LONG_DEFAULT_VAR', longDefault);
expect(result).toBe(longDefault);
});
it('should handle special characters in default value', () => {
delete process.env.SPECIAL_DEFAULT_VAR;
const result = getOptionalEnv('SPECIAL_DEFAULT_VAR', '@#$%^&*()');
expect(result).toBe('@#$%^&*()');
});
});
describe('error conditions', () => {
it('should throw TypeError when varName is not a string', () => {
expect(() => getOptionalEnv(null as any)).toThrow();
});
it('should throw TypeError when varName is undefined', () => {
expect(() => getOptionalEnv(undefined as any)).toThrow();
});
it('should throw TypeError when varName is a number', () => {
expect(() => getOptionalEnv(123 as any)).toThrow();
});
it('should throw TypeError when varName is an object', () => {
expect(() => getOptionalEnv({} as any)).toThrow();
});
it('should throw when varName is empty string', () => {
expect(() => getOptionalEnv('')).toThrow();
});
});
describe('integration scenarios', () => {
it('should work correctly when called multiple times for same variable', () => {
process.env.MULTI_OPTIONAL = 'value';
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
expect(getOptionalEnv('MULTI_OPTIONAL')).toBe('value');
});
it('should work correctly with different default values on multiple calls', () => {
delete process.env.DEFAULT_MULTI;
expect(getOptionalEnv('DEFAULT_MULTI', 'default1')).toBe('default1');
expect(getOptionalEnv('DEFAULT_MULTI', 'default2')).toBe('default2');
expect(getOptionalEnv('DEFAULT_MULTI')).toBeUndefined();
});
it('should reflect environment changes between calls', () => {
delete process.env.CHANGING_OPTIONAL;
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('default');
process.env.CHANGING_OPTIONAL = 'new-value';
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('new-value');
delete process.env.CHANGING_OPTIONAL;
expect(getOptionalEnv('CHANGING_OPTIONAL', 'default')).toBe('default');
});
it('should work alongside getRequiredEnv for different variables', () => {
process.env.REQUIRED_VAR = 'required';
process.env.OPTIONAL_VAR = 'optional';
expect(getRequiredEnv('REQUIRED_VAR')).toBe('required');
expect(getOptionalEnv('OPTIONAL_VAR')).toBe('optional');
});
});
});
describe('validateEnv', () => {
describe('happy path', () => {
it('should validate all required variables successfully', () => {
process.env.VAR1 = 'value1';
process.env.VAR2 = 'value2';
process.env.VAR3 = 'value3';
expect(() => validateEnv(['VAR1', 'VAR2', 'VAR3'])).not.toThrow();
});
it('should validate single required variable', () => {
process.env.SINGLE_VAR = 'value';
expect(() => validateEnv(['SINGLE_VAR'])).not.toThrow();
});
it('should validate empty array without throwing', () => {
expect(() => validateEnv([])).not.toThrow();
});
it('should validate variables with special characters in values', () => {
process.env.SPECIAL1 = 'value!@#$%';
process.env.SPECIAL2 = 'value^&*()';
expect(() => validateEnv(['SPECIAL1', 'SPECIAL2'])).not.toThrow();
});
it('should validate variables with numeric values', () => {
process.env.NUM1 = '123';
process.env.NUM2 = '456.789';
expect(() => validateEnv(['NUM1', 'NUM2'])).not.toThrow();
});
it('should validate variables with boolean string values', () => {
process.env.BOOL1 = 'true';
process.env.BOOL2 = 'false';
expect(() => validateEnv(['BOOL1', 'BOOL2'])).not.toThrow();
});
it('should validate variables with URL values', () => {
process.env.URL1 = 'https://example.com';
process.env.URL2 = 'http://localhost:3000';
expect(() => validateEnv(['URL1', 'URL2'])).not.toThrow();
});
it('should validate variables with JSON string values', () => {
process.env.JSON1 = '{"key":"value"}';
process.env.JSON2 = '[1,2,3]';
expect(() => validateEnv(['JSON1', 'JSON2'])).not.toThrow();
});
it('should validate many variables at once', () => {
for (let i = 0; i < 50; i++) {
process.env[`VAR_${i}`] = `value_${i}`;
}
const varNames = Array.from({ length: 50 }, (_, i) => `VAR_${i}`);
expect(() => validateEnv(varNames)).not.toThrow();
});
});
describe('edge cases', () => {
it('should throw when one required variable is missing', () => {
process.env.PRESENT = 'value';
delete process.env.MISSING;
expect(() => validateEnv(['PRESENT', 'MISSING'])).toThrow();
});
it('should throw when multiple required variables are missing', () => {
delete process.env.MISSING1;
delete process.env.MISSING2;
expect(() => validateEnv(['MISSING1', 'MISSING2'])).toThrow();
});
it('should throw when variable is empty string', () => {
process.env.EMPTY = '';
expect(() => validateEnv(['EMPTY'])).toThrow();
});
it('should throw when variable is whitespace only', () => {
process.env.WHITESPACE = ' ';
expect(() => validateEnv(['WHITESPACE'])).toThrow();
});
it('should throw descriptive error for missing variables', () => {
delete process.env.MISSING_VAR;
expect(() => validateEnv(['MISSING_VAR'])).toThrow('MISSING_VAR');
});
it('should handle duplicate variable names in array', () => {
process.env.DUPLICATE = 'value';
expect(() => validateEnv(['DUPLICATE', 'DUPLICATE'])).not.toThrow();
});
it('should handle variable names with special characters', () => {
process.env['VAR-WITH-DASH'] = 'value';
process.env['VAR_WITH_UNDERSCORE'] = 'value';
expect(() => validateEnv(['VAR-WITH-DASH', 'VAR_WITH_UNDERSCORE'])).not.toThrow();
});
it('should validate single character variable names', () => {
process.env.A = 'value';
process.env.B = 'value';
expect(() => validateEnv(['A', 'B'])).not.toThrow();
});
it('should validate very long variable names', () => {
const longName1 = 'VAR_' + 'A'.repeat(100);
const longName2 = 'VAR_' + 'B'.repeat(100);
process.env[longName1] = 'value';
process.env[longName2] = 'value';
expect(() => validateEnv([longName1, longName2])).not.toThrow();
});
it('should accept "0" as valid value', () => {
process.env.ZERO = '0';
expect(() => validateEnv(['ZERO'])).not.toThrow();
});
it('should accept "false" as valid value', () => {
process.env.FALSE_STR = 'false';
expect(() => validateEnv(['FALSE_STR'])).not.toThrow();
});
});
describe('error conditions', () => {
it('should throw TypeError when varNames is not an array', () => {
expect(() => validateEnv(null as any)).toThrow();
});
it('should throw TypeError when varNames is undefined', () => {
expect(() => validateEnv(undefined as any)).toThrow();
});
it('should throw TypeError when varNames is a string', () => {
expect(() => validateEnv('VAR' as any)).toThrow();
});
it('should throw TypeError when varNames is a number', () => {
expect(() => validateEnv(123 as any)).toThrow();
});
it('should throw TypeError when varNames is an object', () => {
expect(() => validateEnv({} as any)).toThrow();
});
it('should throw when array contains non-string elements', () => {
expect(() => validateEnv([123, 'VAR'] as any)).toThrow();
});
it('should throw when array contains null', () => {
expect(() => validateEnv([null, 'VAR'] as any)).toThrow();
});
it('should throw when array contains undefined', () => {
expect(() => validateEnv([undefined, 'VAR'] as any)).toThrow();
});
it('should throw when array contains objects', () => {
expect(() => validateEnv([{}, 'VAR'] as any)).toThrow();
});
it('should throw when array contains empty string', () => {
expect(() => validateEnv(['', 'VAR'])).toThrow();
});
it('should throw when array contains whitespace-only string', () => {
expect(() => validateEnv([' ', 'VAR'])).toThrow();
});
});
describe('error messages', () => {
it('should include all missing variable names in error message', () => {
delete process.env.MISSING1;
delete process.env.MISSING2;
delete process.env.MISSING3;
try {
validateEnv(['MISSING1', 'MISSING2', 'MISSING3']);
expect.fail('Should have thrown an error');
} catch (error) {
expect(error.message).toContain('MISSING1');
expect(error.message).toContain('MISSING2');
expect(error.message).toContain('MISSING3');
}
});
it('should not include present variables in error message', () => {
process.env.PRESENT = 'value';
delete process.env.MISSING;
try {
validateEnv(['PRESENT', 'MISSING']);
expect.fail('Should have thrown an error');
} catch (error) {
expect(error.message).not.toContain('PRESENT');
expect(error.message).toContain('MISSING');
}
});
it('should throw clear error when no variables are provided but required', () => {
try {
validateEnv(null as any);
expect.fail('Should have thrown an error');
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe('integration scenarios', () => {
it('should work correctly when called multiple times', () => {
process.env.VAR1 = 'value1';
process.env.VAR2 = 'value2';
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
expect(() => validateEnv(['VAR1', 'VAR2'])).not.toThrow();
});
it('should reflect environment changes between calls', () => {
process.env.DYNAMIC = 'value';
expect(() => validateEnv(['DYNAMIC'])).not.toThrow();
delete process.env.DYNAMIC;
expect(() => validateEnv(['DYNAMIC'])).toThrow();
process.env.DYNAMIC = 'new-value';
expect(() => validateEnv(['DYNAMIC'])).not.toThrow();
});
it('should work with getRequiredEnv and getOptionalEnv', () => {
process.env.REQUIRED1 = 'value1';
process.env.REQUIRED2 = 'value2';
process.env.OPTIONAL1 = 'value3';
expect(() => validateEnv(['REQUIRED1', 'REQUIRED2'])).not.toThrow();
expect(getRequiredEnv('REQUIRED1')).toBe('value1');
expect(getRequiredEnv('REQUIRED2')).toBe('value2');
expect(getOptionalEnv('OPTIONAL1')).toBe('value3');
});
it('should validate subset of environment variables', () => {
process.env.VAR1 = 'value1';
process.env.VAR2 = 'value2';
process.env.VAR3 = 'value3';
process.env.VAR4 = 'value4';
expect(() => validateEnv(['VAR1', 'VAR3'])).not.toThrow();
expect(() => validateEnv(['VAR2', 'VAR4'])).not.toThrow();
});
it('should handle validation of overlapping sets', () => {
process.env.A = 'valueA';
process.env.B = 'valueB';
process.env.C = 'valueC';
expect(() => validateEnv(['A', 'B'])).not.toThrow();
expect(() => validateEnv(['B', 'C'])).not.toThrow();
expect(() => validateEnv(['A', 'C'])).not.toThrow();
});
});
describe('performance and stress tests', () => {
it('should handle validation of large number of variables efficiently', () => {
const varCount = 1000;
for (let i = 0; i < varCount; i++) {
process.env[`PERF_VAR_${i}`] = `value_${i}`;
}
const varNames = Array.from({ length: varCount }, (_, i) => `PERF_VAR_${i}`);
const startTime = Date.now();
expect(() => validateEnv(varNames)).not.toThrow();
const endTime = Date.now();
// Should complete in reasonable time (less than 1 second for 1000 vars)
expect(endTime - startTime).toBeLessThan(1000);
});
it('should handle validation with very long variable values', () => {
const longValue = 'x'.repeat(100000);
process.env.LONG_VALUE_VAR = longValue;
expect(() => validateEnv(['LONG_VALUE_VAR'])).not.toThrow();
});
});
});
describe('cross-function integration tests', () => {
it('should use all three functions together in typical workflow', () => {
process.env.DATABASE_URL = 'postgres://localhost:5432/mydb';
process.env.API_KEY = 'secret-key-123';
process.env.DEBUG_MODE = 'true';
process.env.OPTIONAL_FEATURE = 'enabled';
// Validate required vars
expect(() => validateEnv(['DATABASE_URL', 'API_KEY', 'DEBUG_MODE'])).not.toThrow();
// Get required vars
const dbUrl = getRequiredEnv('DATABASE_URL');
const apiKey = getRequiredEnv('API_KEY');
// Get optional vars
const optionalFeature = getOptionalEnv('OPTIONAL_FEATURE', 'disabled');
const missingFeature = getOptionalEnv('MISSING_FEATURE', 'default');
expect(dbUrl).toBe('postgres://localhost:5432/mydb');
expect(apiKey).toBe('secret-key-123');
expect(optionalFeature).toBe('enabled');
expect(missingFeature).toBe('default');
});
it('should handle mixed scenarios with some variables present and some missing', () => {
process.env.PRESENT1 = 'value1';
process.env.PRESENT2 = 'value2';
delete process.env.MISSING1;
delete process.env.MISSING2;
expect(() => validateEnv(['PRESENT1', 'PRESENT2'])).not.toThrow();
expect(getRequiredEnv('PRESENT1')).toBe('value1');
expect(getOptionalEnv('MISSING1', 'default')).toBe('default');
expect(() => getRequiredEnv('MISSING2')).toThrow();
});
it('should handle environment initialization pattern', () => {
// Simulate loading environment variables
const requiredVars = ['APP_NAME', 'PORT', 'NODE_ENV'];
process.env.APP_NAME = 'MyApp';
process.env.PORT = '3000';
process.env.NODE_ENV = 'development';
// Validate all required vars at startup
expect(() => validateEnv(requiredVars)).not.toThrow();
// Load individual vars
const appName = getRequiredEnv('APP_NAME');
const port = getRequiredEnv('PORT');
const nodeEnv = getRequiredEnv('NODE_ENV');
const logLevel = getOptionalEnv('LOG_LEVEL', 'info');
expect(appName).toBe('MyApp');
expect(port).toBe('3000');
expect(nodeEnv).toBe('development');
expect(logLevel).toBe('info');
});
});
describe('type safety and TypeScript integration', () => {
it('should work with TypeScript type inference for required env', () => {
process.env.TYPED_VAR = 'typed-value';
const value: string = getRequiredEnv('TYPED_VAR');
expect(value).toBe('typed-value');
});
it('should work with TypeScript type inference for optional env', () => {
process.env.TYPED_OPTIONAL = 'typed-optional-value';
const value: string | undefined = getOptionalEnv('TYPED_OPTIONAL');
expect(value).toBe('typed-optional-value');
});
it('should work with TypeScript type inference for optional env with default', () => {
delete process.env.TYPED_WITH_DEFAULT;
const value: string = getOptionalEnv('TYPED_WITH_DEFAULT', 'default-value')!;
expect(value).toBe('default-value');
});
it('should handle validateEnv with readonly arrays', () => {
process.env.READONLY1 = 'value1';
process.env.READONLY2 = 'value2';
const vars: readonly string[] = ['READONLY1', 'READONLY2'] as const;
expect(() => validateEnv(vars as string[])).not.toThrow();
});
});
describe('real-world scenario tests', () => {
it('should handle typical database configuration', () => {
process.env.DB_HOST = 'localhost';
process.env.DB_PORT = '5432';
process.env.DB_NAME = 'myapp';
process.env.DB_USER = 'admin';
process.env.DB_PASSWORD = 'secret123';
const dbConfig = ['DB_HOST', 'DB_PORT', 'DB_NAME', 'DB_USER', 'DB_PASSWORD'];
expect(() => validateEnv(dbConfig)).not.toThrow();
const host = getRequiredEnv('DB_HOST');
const port = getRequiredEnv('DB_PORT');
const dbName = getRequiredEnv('DB_NAME');
const sslMode = getOptionalEnv('DB_SSL_MODE', 'prefer');
expect(host).toBe('localhost');
expect(port).toBe('5432');
expect(dbName).toBe('myapp');
expect(sslMode).toBe('prefer');
});
it('should handle API service configuration', () => {
process.env.API_BASE_URL = 'https://api.example.com';
process.env.API_KEY = 'key-12345';
process.env.API_TIMEOUT = '30000';
process.env.API_RETRY_COUNT = '3';
expect(() => validateEnv(['API_BASE_URL', 'API_KEY'])).not.toThrow();
const baseUrl = getRequiredEnv('API_BASE_URL');
const apiKey = getRequiredEnv('API_KEY');
const timeout = getOptionalEnv('API_TIMEOUT', '5000');
const retryCount = getOptionalEnv('API_RETRY_COUNT', '1');
const rateLimit = getOptionalEnv('API_RATE_LIMIT');
expect(baseUrl).toBe('https://api.example.com');
expect(apiKey).toBe('key-12345');
expect(timeout).toBe('30000');
expect(retryCount).toBe('3');
expect(rateLimit).toBeUndefined();
});
it('should handle AWS credentials configuration', () => {
process.env.AWS_REGION = 'us-east-1';
process.env.AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE';
process.env.AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';
const awsVars = ['AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'];
expect(() => validateEnv(awsVars)).not.toThrow();
const region = getRequiredEnv('AWS_REGION');
const accessKeyId = getRequiredEnv('AWS_ACCESS_KEY_ID');
const sessionToken = getOptionalEnv('AWS_SESSION_TOKEN');
expect(region).toBe('us-east-1');
expect(accessKeyId).toBe('AKIAIOSFODNN7EXAMPLE');
expect(sessionToken).toBeUndefined();
});
it('should handle feature flags configuration', () => {
process.env.FEATURE_NEW_UI = 'true';
process.env.FEATURE_BETA_API = 'false';
process.env.FEATURE_ANALYTICS = 'enabled';
const newUI = getOptionalEnv('FEATURE_NEW_UI', 'false');
const betaAPI = getOptionalEnv('FEATURE_BETA_API', 'false');
const analytics = getOptionalEnv('FEATURE_ANALYTICS', 'disabled');
const darkMode = getOptionalEnv('FEATURE_DARK_MODE', 'auto');
expect(newUI).toBe('true');
expect(betaAPI).toBe('false');
expect(analytics).toBe('enabled');
expect(darkMode).toBe('auto');
});
it('should handle multi-environment deployment configuration', () => {
process.env.NODE_ENV = 'production';
process.env.APP_VERSION = '1.2.3';
process.env.BUILD_NUMBER = '456';
process.env.DEPLOY_REGION = 'us-west-2';
expect(() => validateEnv(['NODE_ENV'])).not.toThrow();
const env = getRequiredEnv('NODE_ENV');
const version = getOptionalEnv('APP_VERSION', '0.0.0');
const buildNumber = getOptionalEnv('BUILD_NUMBER', 'local');
const region = getOptionalEnv('DEPLOY_REGION', 'us-east-1');
expect(env).toBe('production');
expect(version).toBe('1.2.3');
expect(buildNumber).toBe('456');
expect(region).toBe('us-west-2');
});
});
});

View File

@@ -0,0 +1,393 @@
import { setActivePinia, createPinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useRightSidePanelStore } from './rightSidePanelStore'
describe('rightSidePanelStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
describe('Initial State', () => {
it('initializes with default values', () => {
const store = useRightSidePanelStore()
expect(store.isOpen).toBe(false)
expect(store.activeTab).toBe('parameters')
})
it('creates a new instance for each pinia context', () => {
const store1 = useRightSidePanelStore()
const pinia2 = createPinia()
setActivePinia(pinia2)
const store2 = useRightSidePanelStore()
store1.isOpen = true
expect(store2.isOpen).toBe(false)
})
})
describe('openPanel', () => {
it('opens panel and sets active tab', () => {
const store = useRightSidePanelStore()
store.openPanel('settings')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('settings')
})
it('opens panel with parameters tab', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('parameters')
})
it('opens panel with info tab', () => {
const store = useRightSidePanelStore()
store.openPanel('info')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('info')
})
it('can switch tabs while panel is already open', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
expect(store.activeTab).toBe('parameters')
store.openPanel('settings')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('settings')
})
it('overwrites previous active tab', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
store.openPanel('info')
expect(store.activeTab).toBe('info')
expect(store.activeTab).not.toBe('parameters')
})
})
describe('closePanel', () => {
it('closes an open panel', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
expect(store.isOpen).toBe(true)
store.closePanel()
expect(store.isOpen).toBe(false)
})
it('maintains active tab when closing', () => {
const store = useRightSidePanelStore()
store.openPanel('settings')
const tabBeforeClose = store.activeTab
store.closePanel()
expect(store.activeTab).toBe(tabBeforeClose)
})
it('is idempotent when called multiple times', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
store.closePanel()
store.closePanel()
store.closePanel()
expect(store.isOpen).toBe(false)
})
it('works correctly when panel is already closed', () => {
const store = useRightSidePanelStore()
expect(store.isOpen).toBe(false)
store.closePanel()
expect(store.isOpen).toBe(false)
})
})
describe('togglePanel', () => {
it('opens closed panel', () => {
const store = useRightSidePanelStore()
expect(store.isOpen).toBe(false)
store.togglePanel()
expect(store.isOpen).toBe(true)
})
it('closes open panel', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
expect(store.isOpen).toBe(true)
store.togglePanel()
expect(store.isOpen).toBe(false)
})
it('alternates state on repeated calls', () => {
const store = useRightSidePanelStore()
expect(store.isOpen).toBe(false)
store.togglePanel()
expect(store.isOpen).toBe(true)
store.togglePanel()
expect(store.isOpen).toBe(false)
store.togglePanel()
expect(store.isOpen).toBe(true)
})
it('preserves active tab when toggling', () => {
const store = useRightSidePanelStore()
store.openPanel('settings')
const originalTab = store.activeTab
store.togglePanel()
store.togglePanel()
expect(store.activeTab).toBe(originalTab)
})
})
describe('Active Tab Management', () => {
it('defaults to parameters tab', () => {
const store = useRightSidePanelStore()
expect(store.activeTab).toBe('parameters')
})
it('allows all valid tab types', () => {
const store = useRightSidePanelStore()
const validTabs: Array<'parameters' | 'settings' | 'info'> = [
'parameters',
'settings',
'info'
]
validTabs.forEach(tab => {
store.openPanel(tab)
expect(store.activeTab).toBe(tab)
})
})
it('can be updated directly', () => {
const store = useRightSidePanelStore()
store.activeTab = 'settings'
expect(store.activeTab).toBe('settings')
store.activeTab = 'info'
expect(store.activeTab).toBe('info')
})
})
describe('State Persistence', () => {
it('maintains state across multiple operations', () => {
const store = useRightSidePanelStore()
store.openPanel('parameters')
store.closePanel()
store.openPanel('settings')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('settings')
})
it('independent operations do not interfere', () => {
const store = useRightSidePanelStore()
// Set active tab directly
store.activeTab = 'info'
expect(store.isOpen).toBe(false)
// Toggle panel
store.togglePanel()
expect(store.activeTab).toBe('info')
expect(store.isOpen).toBe(true)
})
})
describe('Edge Cases', () => {
it('handles rapid toggle operations', () => {
const store = useRightSidePanelStore()
for (let i = 0; i < 100; i++) {
store.togglePanel()
}
// Should be closed (even number of toggles)
expect(store.isOpen).toBe(false)
})
it('handles rapid tab switching', () => {
const store = useRightSidePanelStore()
const tabs: Array<'parameters' | 'settings' | 'info'> = [
'parameters',
'settings',
'info'
]
for (let i = 0; i < 50; i++) {
const tab = tabs[i % tabs.length]
store.openPanel(tab)
}
// Should end on info tab (50 % 3 = 2, which is 'info')
expect(store.activeTab).toBe('info')
expect(store.isOpen).toBe(true)
})
it('maintains type safety', () => {
const store = useRightSidePanelStore()
// TypeScript should enforce correct types
store.openPanel('parameters')
store.openPanel('settings')
store.openPanel('info')
expect(['parameters', 'settings', 'info']).toContain(store.activeTab)
})
})
describe('Workflow Scenarios', () => {
it('supports typical user workflow: open, switch tabs, close', () => {
const store = useRightSidePanelStore()
// User opens parameters
store.openPanel('parameters')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('parameters')
// User switches to settings
store.openPanel('settings')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('settings')
// User closes panel
store.closePanel()
expect(store.isOpen).toBe(false)
})
it('supports quick toggle workflow', () => {
const store = useRightSidePanelStore()
// User toggles panel (opens with default tab)
store.togglePanel()
expect(store.isOpen).toBe(true)
// User quickly toggles to close
store.togglePanel()
expect(store.isOpen).toBe(false)
})
it('supports switching to info tab from external action', () => {
const store = useRightSidePanelStore()
// Panel might be closed
expect(store.isOpen).toBe(false)
// User clicks "Info" button which opens panel to info tab
store.openPanel('info')
expect(store.isOpen).toBe(true)
expect(store.activeTab).toBe('info')
})
})
describe('Integration with UI', () => {
it('supports binding to panel visibility', () => {
const store = useRightSidePanelStore()
// Component v-if="store.isOpen"
expect(store.isOpen).toBe(false)
store.openPanel('parameters')
expect(store.isOpen).toBe(true)
})
it('supports binding to active tab', () => {
const store = useRightSidePanelStore()
store.openPanel('settings')
// Component v-if="store.activeTab === 'settings'"
expect(store.activeTab).toBe('settings')
})
it('supports toggle button binding', () => {
const store = useRightSidePanelStore()
// Button @click="store.togglePanel"
const initialState = store.isOpen
store.togglePanel()
expect(store.isOpen).toBe(!initialState)
})
})
describe('Reactive Properties', () => {
it('isOpen is reactive', () => {
const store = useRightSidePanelStore()
const values: boolean[] = []
// Simulating a watcher
const stopWatch = vi.fn(() => {
values.push(store.isOpen)
})
stopWatch()
store.togglePanel()
stopWatch()
store.togglePanel()
stopWatch()
expect(values).toEqual([false, true, false])
})
it('activeTab is reactive', () => {
const store = useRightSidePanelStore()
const tabs: string[] = []
const recordTab = () => tabs.push(store.activeTab)
recordTab()
store.openPanel('settings')
recordTab()
store.openPanel('info')
recordTab()
expect(tabs).toEqual(['parameters', 'settings', 'info'])
})
})
describe('Type Safety', () => {
it('enforces correct tab types at runtime', () => {
const store = useRightSidePanelStore()
// These should work
store.openPanel('parameters')
store.openPanel('settings')
store.openPanel('info')
// TypeScript should prevent invalid tabs at compile time
// @ts-expect-error - testing invalid tab
// store.openPanel('invalid')
expect(store.activeTab).toBe('info')
})
})
})