merge main
commit
3b8d96f45c
@ -1,41 +0,0 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 16081485540c
|
||||
Revises: d28f2004b072
|
||||
Create Date: 2025-05-15 16:35:39.113777
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '16081485540c'
|
||||
down_revision = '2adcbe1f5dfb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tenant_plugin_auto_upgrade_strategies',
|
||||
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
|
||||
sa.Column('strategy_setting', sa.String(length=16), server_default='fix_only', nullable=False),
|
||||
sa.Column('upgrade_time_of_day', sa.Integer(), nullable=False),
|
||||
sa.Column('upgrade_mode', sa.String(length=16), server_default='exclude', nullable=False),
|
||||
sa.Column('exclude_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
|
||||
sa.Column('include_plugins', sa.ARRAY(sa.String(length=255)), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='tenant_plugin_auto_upgrade_strategy_pkey'),
|
||||
sa.UniqueConstraint('tenant_id', name='unique_tenant_plugin_auto_upgrade_strategy')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('tenant_plugin_auto_upgrade_strategies')
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,150 @@
|
||||
import pytest
|
||||
|
||||
from services.auth.auth_type import AuthType
|
||||
|
||||
|
||||
class TestAuthType:
|
||||
"""Test cases for AuthType enum"""
|
||||
|
||||
def test_auth_type_is_str_enum(self):
|
||||
"""Test that AuthType is properly a StrEnum"""
|
||||
assert issubclass(AuthType, str)
|
||||
assert hasattr(AuthType, "__members__")
|
||||
|
||||
def test_auth_type_has_expected_values(self):
|
||||
"""Test that all expected auth types exist with correct values"""
|
||||
expected_values = {
|
||||
"FIRECRAWL": "firecrawl",
|
||||
"WATERCRAWL": "watercrawl",
|
||||
"JINA": "jinareader",
|
||||
}
|
||||
|
||||
# Verify all expected members exist
|
||||
for member_name, expected_value in expected_values.items():
|
||||
assert hasattr(AuthType, member_name)
|
||||
assert getattr(AuthType, member_name).value == expected_value
|
||||
|
||||
# Verify no extra members exist
|
||||
assert len(AuthType) == len(expected_values)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("auth_type", "expected_string"),
|
||||
[
|
||||
(AuthType.FIRECRAWL, "firecrawl"),
|
||||
(AuthType.WATERCRAWL, "watercrawl"),
|
||||
(AuthType.JINA, "jinareader"),
|
||||
],
|
||||
)
|
||||
def test_auth_type_string_representation(self, auth_type, expected_string):
|
||||
"""Test string representation of auth types"""
|
||||
assert str(auth_type) == expected_string
|
||||
assert auth_type.value == expected_string
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("auth_type", "compare_value", "expected_result"),
|
||||
[
|
||||
(AuthType.FIRECRAWL, "firecrawl", True),
|
||||
(AuthType.WATERCRAWL, "watercrawl", True),
|
||||
(AuthType.JINA, "jinareader", True),
|
||||
(AuthType.FIRECRAWL, "FIRECRAWL", False), # Case sensitive
|
||||
(AuthType.FIRECRAWL, "watercrawl", False),
|
||||
(AuthType.JINA, "jina", False), # Full value mismatch
|
||||
],
|
||||
)
|
||||
def test_auth_type_comparison(self, auth_type, compare_value, expected_result):
|
||||
"""Test auth type comparison with strings"""
|
||||
assert (auth_type == compare_value) is expected_result
|
||||
|
||||
def test_auth_type_iteration(self):
|
||||
"""Test that AuthType can be iterated over"""
|
||||
auth_types = list(AuthType)
|
||||
assert len(auth_types) == 3
|
||||
assert AuthType.FIRECRAWL in auth_types
|
||||
assert AuthType.WATERCRAWL in auth_types
|
||||
assert AuthType.JINA in auth_types
|
||||
|
||||
def test_auth_type_membership(self):
|
||||
"""Test membership checking for AuthType"""
|
||||
assert "firecrawl" in [auth.value for auth in AuthType]
|
||||
assert "watercrawl" in [auth.value for auth in AuthType]
|
||||
assert "jinareader" in [auth.value for auth in AuthType]
|
||||
assert "invalid" not in [auth.value for auth in AuthType]
|
||||
|
||||
def test_auth_type_invalid_attribute_access(self):
|
||||
"""Test accessing non-existent auth type raises AttributeError"""
|
||||
with pytest.raises(AttributeError):
|
||||
_ = AuthType.INVALID_TYPE
|
||||
|
||||
def test_auth_type_immutability(self):
|
||||
"""Test that enum values cannot be modified"""
|
||||
# In Python 3.11+, enum members are read-only
|
||||
with pytest.raises(AttributeError):
|
||||
AuthType.FIRECRAWL = "modified"
|
||||
|
||||
def test_auth_type_from_value(self):
|
||||
"""Test creating AuthType from string value"""
|
||||
assert AuthType("firecrawl") == AuthType.FIRECRAWL
|
||||
assert AuthType("watercrawl") == AuthType.WATERCRAWL
|
||||
assert AuthType("jinareader") == AuthType.JINA
|
||||
|
||||
# Test invalid value
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
AuthType("invalid_auth_type")
|
||||
assert "invalid_auth_type" in str(exc_info.value)
|
||||
|
||||
def test_auth_type_name_property(self):
|
||||
"""Test the name property of enum members"""
|
||||
assert AuthType.FIRECRAWL.name == "FIRECRAWL"
|
||||
assert AuthType.WATERCRAWL.name == "WATERCRAWL"
|
||||
assert AuthType.JINA.name == "JINA"
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"auth_type",
|
||||
[AuthType.FIRECRAWL, AuthType.WATERCRAWL, AuthType.JINA],
|
||||
)
|
||||
def test_auth_type_isinstance_checks(self, auth_type):
|
||||
"""Test isinstance checks for auth types"""
|
||||
assert isinstance(auth_type, AuthType)
|
||||
assert isinstance(auth_type, str)
|
||||
assert isinstance(auth_type.value, str)
|
||||
|
||||
def test_auth_type_hash(self):
|
||||
"""Test that auth types are hashable and can be used in sets/dicts"""
|
||||
auth_set = {AuthType.FIRECRAWL, AuthType.WATERCRAWL, AuthType.JINA}
|
||||
assert len(auth_set) == 3
|
||||
|
||||
auth_dict = {
|
||||
AuthType.FIRECRAWL: "firecrawl_handler",
|
||||
AuthType.WATERCRAWL: "watercrawl_handler",
|
||||
AuthType.JINA: "jina_handler",
|
||||
}
|
||||
assert auth_dict[AuthType.FIRECRAWL] == "firecrawl_handler"
|
||||
|
||||
def test_auth_type_json_serializable(self):
|
||||
"""Test that auth types can be JSON serialized"""
|
||||
import json
|
||||
|
||||
auth_data = {
|
||||
"provider": AuthType.FIRECRAWL,
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
# Should serialize to string value
|
||||
json_str = json.dumps(auth_data, default=str)
|
||||
assert '"provider": "firecrawl"' in json_str
|
||||
|
||||
def test_auth_type_matches_factory_usage(self):
|
||||
"""Test that all AuthType values are handled by ApiKeyAuthFactory"""
|
||||
# This test verifies that the enum values match what's expected
|
||||
# by the factory implementation
|
||||
from services.auth.api_key_auth_factory import ApiKeyAuthFactory
|
||||
|
||||
for auth_type in AuthType:
|
||||
# Should not raise ValueError for valid auth types
|
||||
try:
|
||||
auth_class = ApiKeyAuthFactory.get_apikey_auth_factory(auth_type)
|
||||
assert auth_class is not None
|
||||
except ImportError:
|
||||
# It's OK if the actual auth implementation doesn't exist
|
||||
# We're just testing that the enum value is recognized
|
||||
pass
|
||||
@ -0,0 +1,105 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiAlertFill } from '@remixicon/react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { snakeCase2CamelCase } from '@/utils/format'
|
||||
import { useMixedTranslation } from '../marketplace/hooks'
|
||||
|
||||
type DeprecationNoticeProps = {
|
||||
status: 'deleted' | 'active'
|
||||
deprecatedReason: string
|
||||
alternativePluginId: string
|
||||
alternativePluginURL: string
|
||||
locale?: string
|
||||
className?: string
|
||||
innerWrapperClassName?: string
|
||||
iconWrapperClassName?: string
|
||||
textClassName?: string
|
||||
}
|
||||
|
||||
const i18nPrefix = 'plugin.detailPanel.deprecation'
|
||||
|
||||
const DeprecationNotice: FC<DeprecationNoticeProps> = ({
|
||||
status,
|
||||
deprecatedReason,
|
||||
alternativePluginId,
|
||||
alternativePluginURL,
|
||||
locale,
|
||||
className,
|
||||
innerWrapperClassName,
|
||||
iconWrapperClassName,
|
||||
textClassName,
|
||||
}) => {
|
||||
const { t } = useMixedTranslation(locale)
|
||||
|
||||
const deprecatedReasonKey = useMemo(() => {
|
||||
if (!deprecatedReason) return ''
|
||||
return snakeCase2CamelCase(deprecatedReason)
|
||||
}, [deprecatedReason])
|
||||
|
||||
// Check if the deprecatedReasonKey exists in i18n
|
||||
const hasValidDeprecatedReason = useMemo(() => {
|
||||
if (!deprecatedReason || !deprecatedReasonKey) return false
|
||||
|
||||
// Define valid reason keys that exist in i18n
|
||||
const validReasonKeys = ['businessAdjustments', 'ownershipTransferred', 'noMaintainer']
|
||||
return validReasonKeys.includes(deprecatedReasonKey)
|
||||
}, [deprecatedReason, deprecatedReasonKey])
|
||||
|
||||
if (status !== 'deleted')
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
<div className={cn(
|
||||
'relative flex items-start gap-x-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]',
|
||||
innerWrapperClassName,
|
||||
)}>
|
||||
<div className='absolute left-0 top-0 -z-10 h-full w-full bg-toast-warning-bg opacity-40' />
|
||||
<div className={cn('flex size-6 shrink-0 items-center justify-center', iconWrapperClassName)}>
|
||||
<RiAlertFill className='size-4 text-text-warning-secondary' />
|
||||
</div>
|
||||
<div className={cn('system-xs-regular grow py-1 text-text-primary', textClassName)}>
|
||||
{
|
||||
hasValidDeprecatedReason && alternativePluginId && (
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey={`${i18nPrefix}.fullMessage`}
|
||||
components={{
|
||||
CustomLink: (
|
||||
<Link
|
||||
href={alternativePluginURL}
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='underline'
|
||||
/>
|
||||
),
|
||||
}}
|
||||
values={{
|
||||
deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`),
|
||||
alternativePluginId,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasValidDeprecatedReason && !alternativePluginId && (
|
||||
<span>
|
||||
{t(`${i18nPrefix}.onlyReason`, { deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`) })}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
!hasValidDeprecatedReason && (
|
||||
<span>{t(`${i18nPrefix}.noReason`)}</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(DeprecationNotice)
|
||||
@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Workflow Panel Width Persistence Tests
|
||||
* Tests for GitHub issue #22745: Panel width persistence bug fix
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
type PanelWidthSource = 'user' | 'system'
|
||||
|
||||
// Mock localStorage for testing
|
||||
const createMockLocalStorage = () => {
|
||||
const storage: Record<string, string> = {}
|
||||
return {
|
||||
getItem: jest.fn((key: string) => storage[key] || null),
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
storage[key] = value
|
||||
}),
|
||||
removeItem: jest.fn((key: string) => {
|
||||
delete storage[key]
|
||||
}),
|
||||
clear: jest.fn(() => {
|
||||
Object.keys(storage).forEach(key => delete storage[key])
|
||||
}),
|
||||
get storage() { return { ...storage } },
|
||||
}
|
||||
}
|
||||
|
||||
// Core panel width logic extracted from the component
|
||||
const createPanelWidthManager = (storageKey: string) => {
|
||||
return {
|
||||
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||
const newValue = Math.max(400, Math.min(width, 800))
|
||||
if (source === 'user')
|
||||
localStorage.setItem(storageKey, `${newValue}`)
|
||||
|
||||
return newValue
|
||||
},
|
||||
getStoredWidth: () => {
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
return stored ? Number.parseFloat(stored) : 400
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('Workflow Panel Width Persistence', () => {
|
||||
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
||||
|
||||
beforeEach(() => {
|
||||
mockLocalStorage = createMockLocalStorage()
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Node Panel Width Management', () => {
|
||||
const storageKey = 'workflow-node-panel-width'
|
||||
|
||||
it('should save user resize to localStorage', () => {
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
const result = manager.updateWidth(500, 'user')
|
||||
|
||||
expect(result).toBe(500)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '500')
|
||||
})
|
||||
|
||||
it('should not save system compression to localStorage', () => {
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
const result = manager.updateWidth(200, 'system')
|
||||
|
||||
expect(result).toBe(400) // Respects minimum width
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should enforce minimum width of 400px', () => {
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
// User tries to set below minimum
|
||||
const userResult = manager.updateWidth(300, 'user')
|
||||
expect(userResult).toBe(400)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith(storageKey, '400')
|
||||
|
||||
// System compression below minimum
|
||||
const systemResult = manager.updateWidth(150, 'system')
|
||||
expect(systemResult).toBe(400)
|
||||
expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call
|
||||
})
|
||||
|
||||
it('should preserve user preferences during system compression', () => {
|
||||
localStorage.setItem(storageKey, '600')
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
// System compresses panel
|
||||
manager.updateWidth(200, 'system')
|
||||
|
||||
// User preference should remain unchanged
|
||||
expect(localStorage.getItem(storageKey)).toBe('600')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bug Scenario Reproduction', () => {
|
||||
it('should reproduce original bug behavior (for comparison)', () => {
|
||||
const storageKey = 'workflow-node-panel-width'
|
||||
|
||||
// Original buggy behavior - always saves regardless of source
|
||||
const buggyUpdate = (width: number) => {
|
||||
localStorage.setItem(storageKey, `${width}`)
|
||||
return Math.max(400, width)
|
||||
}
|
||||
|
||||
localStorage.setItem(storageKey, '500') // User preference
|
||||
buggyUpdate(200) // System compression pollutes localStorage
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe('200') // Bug: corrupted state
|
||||
})
|
||||
|
||||
it('should verify fix prevents localStorage pollution', () => {
|
||||
const storageKey = 'workflow-node-panel-width'
|
||||
const manager = createPanelWidthManager(storageKey)
|
||||
|
||||
localStorage.setItem(storageKey, '500') // User preference
|
||||
manager.updateWidth(200, 'system') // System compression
|
||||
|
||||
expect(localStorage.getItem(storageKey)).toBe('500') // Fix: preserved state
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle multiple rapid operations correctly', () => {
|
||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||
|
||||
// Rapid system adjustments
|
||||
manager.updateWidth(300, 'system')
|
||||
manager.updateWidth(250, 'system')
|
||||
manager.updateWidth(180, 'system')
|
||||
|
||||
// Single user adjustment
|
||||
manager.updateWidth(550, 'user')
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledTimes(1)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('workflow-node-panel-width', '550')
|
||||
})
|
||||
|
||||
it('should handle corrupted localStorage gracefully', () => {
|
||||
localStorage.setItem('workflow-node-panel-width', '150') // Below minimum
|
||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||
|
||||
const storedWidth = manager.getStoredWidth()
|
||||
expect(storedWidth).toBe(150) // Returns raw value
|
||||
|
||||
// User can correct the preference
|
||||
const correctedWidth = manager.updateWidth(500, 'user')
|
||||
expect(correctedWidth).toBe(500)
|
||||
expect(localStorage.getItem('workflow-node-panel-width')).toBe('500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('TypeScript Type Safety', () => {
|
||||
it('should enforce source parameter type', () => {
|
||||
const manager = createPanelWidthManager('workflow-node-panel-width')
|
||||
|
||||
// Valid source values
|
||||
manager.updateWidth(500, 'user')
|
||||
manager.updateWidth(500, 'system')
|
||||
|
||||
// Default to 'user'
|
||||
manager.updateWidth(500)
|
||||
|
||||
expect(localStorage.setItem).toHaveBeenCalledTimes(2) // user + default
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Debug and Preview Panel Width Persistence Tests
|
||||
* Tests for GitHub issue #22745: Panel width persistence bug fix
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
type PanelWidthSource = 'user' | 'system'
|
||||
|
||||
// Mock localStorage for testing
|
||||
const createMockLocalStorage = () => {
|
||||
const storage: Record<string, string> = {}
|
||||
return {
|
||||
getItem: jest.fn((key: string) => storage[key] || null),
|
||||
setItem: jest.fn((key: string, value: string) => {
|
||||
storage[key] = value
|
||||
}),
|
||||
removeItem: jest.fn((key: string) => {
|
||||
delete storage[key]
|
||||
}),
|
||||
clear: jest.fn(() => {
|
||||
Object.keys(storage).forEach(key => delete storage[key])
|
||||
}),
|
||||
get storage() { return { ...storage } },
|
||||
}
|
||||
}
|
||||
|
||||
// Preview panel width logic
|
||||
const createPreviewPanelManager = () => {
|
||||
const storageKey = 'debug-and-preview-panel-width'
|
||||
|
||||
return {
|
||||
updateWidth: (width: number, source: PanelWidthSource = 'user') => {
|
||||
const newValue = Math.max(400, Math.min(width, 800))
|
||||
if (source === 'user')
|
||||
localStorage.setItem(storageKey, `${newValue}`)
|
||||
|
||||
return newValue
|
||||
},
|
||||
getStoredWidth: () => {
|
||||
const stored = localStorage.getItem(storageKey)
|
||||
return stored ? Number.parseFloat(stored) : 400
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('Debug and Preview Panel Width Persistence', () => {
|
||||
let mockLocalStorage: ReturnType<typeof createMockLocalStorage>
|
||||
|
||||
beforeEach(() => {
|
||||
mockLocalStorage = createMockLocalStorage()
|
||||
Object.defineProperty(globalThis, 'localStorage', {
|
||||
value: mockLocalStorage,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Preview Panel Width Management', () => {
|
||||
it('should save user resize to localStorage', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
const result = manager.updateWidth(450, 'user')
|
||||
|
||||
expect(result).toBe(450)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '450')
|
||||
})
|
||||
|
||||
it('should not save system compression to localStorage', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
const result = manager.updateWidth(300, 'system')
|
||||
|
||||
expect(result).toBe(400) // Respects minimum width
|
||||
expect(localStorage.setItem).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should behave identically to Node Panel', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
// Both user and system operations should behave consistently
|
||||
manager.updateWidth(500, 'user')
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
||||
|
||||
manager.updateWidth(200, 'system')
|
||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Dual Panel Scenario', () => {
|
||||
it('should maintain independence from Node Panel', () => {
|
||||
localStorage.setItem('workflow-node-panel-width', '600')
|
||||
localStorage.setItem('debug-and-preview-panel-width', '450')
|
||||
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
// System compresses preview panel
|
||||
manager.updateWidth(200, 'system')
|
||||
|
||||
// Only preview panel storage key should be unaffected
|
||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('450')
|
||||
expect(localStorage.getItem('workflow-node-panel-width')).toBe('600')
|
||||
})
|
||||
|
||||
it('should handle F12 scenario consistently', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
// User sets preference
|
||||
manager.updateWidth(500, 'user')
|
||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||
|
||||
// F12 opens causing viewport compression
|
||||
manager.updateWidth(180, 'system')
|
||||
|
||||
// User preference preserved
|
||||
expect(localStorage.getItem('debug-and-preview-panel-width')).toBe('500')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Consistency with Node Panel', () => {
|
||||
it('should enforce same minimum width rules', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
// Same 400px minimum as Node Panel
|
||||
const result = manager.updateWidth(300, 'user')
|
||||
expect(result).toBe(400)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '400')
|
||||
})
|
||||
|
||||
it('should use same source parameter pattern', () => {
|
||||
const manager = createPreviewPanelManager()
|
||||
|
||||
// Default to 'user' when source not specified
|
||||
manager.updateWidth(500)
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith('debug-and-preview-panel-width', '500')
|
||||
|
||||
// Explicit 'system' source
|
||||
manager.updateWidth(300, 'system')
|
||||
expect(localStorage.setItem).toHaveBeenCalledTimes(1) // Only user call
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue