From 750b143dcde8786ffcd5fe5d9b8ef85f21e561b8 Mon Sep 17 00:00:00 2001 From: limina1 Date: Mon, 23 Jun 2025 16:33:42 -0400 Subject: [PATCH] Cleanup and redundancy removal --- .gitignore | 18 +- docs/event-types-panel-redesign.org | 105 ----- ...sualization-optimization-implementation.md | 332 ------------- ...ualization-optimization-quick-reference.md | 124 ----- .../08-visualization-optimization-summary.md | 168 ------- src/lib/components/EventTypeConfig.svelte | 2 +- src/lib/navigator/EventNetwork/Legend.svelte | 1 - .../navigator/EventNetwork/Settings.svelte | 50 +- src/lib/navigator/EventNetwork/index.svelte | 29 +- .../navigator/EventNetwork/utils/common.ts | 41 ++ .../EventNetwork/utils/forceSimulation.ts | 12 +- .../EventNetwork/utils/networkBuilder.ts | 12 +- .../utils/personNetworkBuilder.ts | 36 +- .../EventNetwork/utils/starNetworkBuilder.ts | 12 +- .../EventNetwork/utils/tagNetworkBuilder.ts | 35 +- src/lib/stores/displayLimits.ts | 19 - src/lib/stores/visualizationConfig.ts | 123 +---- src/lib/utils/displayLimits.ts | 62 +-- src/lib/utils/eventColors.ts | 10 - src/routes/visualize/+page.svelte | 108 +---- tests/e2e/collapsible-sections.pw.spec.ts | 279 ----------- tests/e2e/example.pw.spec.ts | 18 - .../e2e/poc-performance-validation.pw.spec.ts | 365 --------------- tests/e2e/tag-anchor-interactions.pw.spec.ts | 308 ------------- .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../error-context.md | 150 ------ .../displayLimitsIntegration.test.ts | 382 --------------- tests/integration/markupIntegration.test.ts | 99 ---- tests/integration/markupTestfile.md | 244 ---------- tests/unit/advancedMarkupParser.test.ts | 118 ----- tests/unit/basicMarkupParser.test.ts | 88 ---- tests/unit/coordinateDeduplication.test.ts | 376 --------------- tests/unit/linkRenderingDebug.test.ts | 143 ------ .../visualizationReactivity.extended.test.ts | 436 ------------------ 39 files changed, 158 insertions(+), 5047 deletions(-) delete mode 100644 docs/event-types-panel-redesign.org delete mode 100644 docs/mini-projects/08-visualization-optimization-implementation.md delete mode 100644 docs/mini-projects/08-visualization-optimization-quick-reference.md delete mode 100644 docs/mini-projects/08-visualization-optimization-summary.md create mode 100644 src/lib/navigator/EventNetwork/utils/common.ts delete mode 100644 src/lib/stores/displayLimits.ts delete mode 100644 tests/e2e/collapsible-sections.pw.spec.ts delete mode 100644 tests/e2e/example.pw.spec.ts delete mode 100644 tests/e2e/poc-performance-validation.pw.spec.ts delete mode 100644 tests/e2e/tag-anchor-interactions.pw.spec.ts delete mode 100644 tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md delete mode 100644 tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md delete mode 100644 tests/integration/displayLimitsIntegration.test.ts delete mode 100644 tests/integration/markupIntegration.test.ts delete mode 100644 tests/integration/markupTestfile.md delete mode 100644 tests/unit/advancedMarkupParser.test.ts delete mode 100644 tests/unit/basicMarkupParser.test.ts delete mode 100644 tests/unit/coordinateDeduplication.test.ts delete mode 100644 tests/unit/linkRenderingDebug.test.ts delete mode 100644 tests/unit/visualizationReactivity.extended.test.ts diff --git a/.gitignore b/.gitignore index ef18a0a..4338def 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,21 @@ node_modules vite.config.js.timestamp-* vite.config.ts.timestamp-* -# tests -/tests/e2e/html-report/*.html -/tests/e2e/test-results/*.last-run.json +# tests - ignore all test directories and files +/tests/ +/test/ +/__tests__/ +*.test.js +*.test.ts +*.spec.js +*.spec.ts +*.test.svelte +*.spec.svelte +/coverage/ +/.nyc_output/ + +# documentation +/docs/ # Deno /.deno/ diff --git a/docs/event-types-panel-redesign.org b/docs/event-types-panel-redesign.org deleted file mode 100644 index bb898dc..0000000 --- a/docs/event-types-panel-redesign.org +++ /dev/null @@ -1,105 +0,0 @@ -#+TITLE: Navigation Visualization Clean Implementation Plan -#+DATE: [2025-01-17] -#+AUTHOR: gc-alexandria team - -* Overview - -Clean implementation plan for the event network visualization, focusing on performance and stability. - -* Core Principles - -1. **Load once, render many**: Fetch all data upfront, toggle visibility without re-fetching -2. **Simple state management**: Avoid reactive Sets and circular dependencies -3. **Batched operations**: Minimize network requests by combining queries -4. **Clean separation**: UI controls in Legend, visualization logic in index.svelte - -* Implementation Phases - -** Phase 1: Tag Anchor Controls Migration -- +Move tag type selection from Settings to Legend+ -- +Move expansion depth control from Settings to Legend+ -- +Move requirePublications checkbox from Settings to Legend+ -- +Use native HTML button instead of flowbite Toggle component+ -- +Clean up Settings panel+ - -** Phase 2: Person Visualizer -- +Add collapsible "Person Visualizer" section in Legend+ -- +Display all event authors (pubkeys) as list items+ -- +Fetch display names from kind 0 events+ -- +Render person nodes as diamond shapes in graph+ -- +Default all person nodes to disabled state+ -- +Click to toggle individual person visibility+ - -** Phase 3: State Management Fixes -- Replace reactive Set with object/map for disabled states -- Use $derived for computed values to avoid circular updates -- Defer state updates with setTimeout where needed -- Simplify $effect dependencies -- Ensure clean data flow without loops - -** Phase 4: Fetch Optimization -- Batch multiple event kinds into single queries -- Combine 30041 and 30818 content fetches -- Pre-fetch all person profiles on initial load -- Cache profile data to avoid re-fetching - -** Phase 5: Load-Once Architecture -- +Fetch ALL configured event kinds upfront (regardless of enabled state)+ -- +Store complete dataset in memory+ -- +Only render nodes that are enabled+ -- +Toggle operations just change visibility, no re-fetch+ -- +Prevents UI freezing on toggle operations+ - -* Technical Details - -** State Structure -#+BEGIN_SRC typescript -// Avoid Sets for reactive state -let disabledTagsMap = $state>({}); -let disabledPersonsMap = $state>({}); - -// Derived for compatibility -const disabledTags = $derived(new Set(Object.keys(disabledTagsMap).filter(k => disabledTagsMap[k]))); -const disabledPersons = $derived(new Set(Object.keys(disabledPersonsMap).filter(k => disabledPersonsMap[k]))); -#+END_SRC - -** Person Node Structure -#+BEGIN_SRC typescript -interface PersonAnchor extends NetworkNode { - type: "PersonAnchor"; - isPersonAnchor: true; - pubkey: string; - displayName?: string; -} -#+END_SRC - -** Batch Fetch Example -#+BEGIN_SRC typescript -// Instead of separate queries -const contentEvents = await $ndkInstance.fetchEvents({ - kinds: [30041, 30818], // Batch multiple kinds - "#d": Array.from(dTags), - limit: combinedLimit -}); -#+END_SRC - -* Benefits - -1. **Performance**: No re-fetching on toggle operations -2. **Stability**: Avoids infinite loops and reactive state issues -3. **UX**: Smooth, instant toggle without freezing -4. **Maintainability**: Clear separation of concerns -5. **Scalability**: Handles large numbers of nodes efficiently - -* Additional Improvements - -** Profile Fetching Optimization -- When follow list limit is 0, only fetch profiles from event authors -- Excludes follow list pubkeys from profile fetching when not needed -- Reduces unnecessary network requests - -** Person Node Visual Distinction -- Green diamonds (#10B981) for authors of displayed events -- Kind 3 color for people from follow lists -- Visual clarity on social graph relationships -- Legend updates to match graph coloring diff --git a/docs/mini-projects/08-visualization-optimization-implementation.md b/docs/mini-projects/08-visualization-optimization-implementation.md deleted file mode 100644 index 96f9300..0000000 --- a/docs/mini-projects/08-visualization-optimization-implementation.md +++ /dev/null @@ -1,332 +0,0 @@ -# Visualization Optimization Implementation Guide - -**Component**: `/src/lib/navigator/EventNetwork/index.svelte` -**Author**: Claude Agent 3 (Master Coordinator) -**Date**: January 6, 2025 - -## Implementation Details - -### 1. Update Type System - -The core of the optimization is a discriminated union type that categorizes parameter changes: - -```typescript -type UpdateType = - | { kind: 'full'; reason: string } - | { kind: 'structural'; reason: string; params: Set } - | { kind: 'visual'; params: Set }; -``` - -### 2. Parameter Tracking - -Track current and previous parameter values to detect changes: - -```typescript -let lastUpdateParams = $state({ - events: events, - eventCount: events?.length || 0, - levels: currentLevels, - star: starVisualization, - tags: showTagAnchors, - tagType: selectedTagType, - disabledCount: disabledTags.size, - tagExpansion: tagExpansionDepth, - theme: isDarkMode -}); -``` - -### 3. Change Detection - -The update detection has been extracted to a utility module: - -```typescript -import { - type UpdateType, - type UpdateParams, - detectChanges, - detectUpdateType as detectUpdateTypeUtil, - logUpdateType -} from "$lib/utils/updateDetection"; -``` - -### 4. Visual Properties Update Function - -The optimized update function that modifies existing elements: - -```typescript -function updateVisualProperties() { - const startTime = performance.now(); - debug("updateVisualProperties called"); - - if (!svgGroup || !simulation || !nodes.length) { - debug("Cannot update visual properties - missing required elements"); - return; - } - - // Update simulation forces based on star mode - if (starVisualization) { - simulation - .force("charge", d3.forceManyBody().strength(-300)) - .force("link", d3.forceLink(links).id((d: any) => d.id).distance(LINK_DISTANCE)) - .force("radial", d3.forceRadial(200, width / 2, height / 2)) - .force("center", null); - } else { - simulation - .force("charge", d3.forceManyBody().strength(-500)) - .force("link", d3.forceLink(links).id((d: any) => d.id).distance(LINK_DISTANCE)) - .force("radial", null) - .force("center", d3.forceCenter(width / 2, height / 2)); - } - - // Update node appearances in-place - svgGroup.selectAll("g.node") - .select("circle.visual-circle") - .attr("class", (d: NetworkNode) => { - // Class updates for star mode - }) - .attr("r", (d: NetworkNode) => { - // Radius updates - }) - .attr("opacity", (d: NetworkNode) => { - // Opacity for disabled tags - }) - .attr("fill", (d: NetworkNode) => { - // Color updates for theme changes - }); - - // Gentle restart - simulation.alpha(0.3).restart(); - - const updateTime = performance.now() - startTime; - debug(`Visual properties updated in ${updateTime.toFixed(2)}ms`); -} -``` - -### 5. Update Routing - -The main effect now routes updates based on type: - -```typescript -$effect(() => { - if (!svg || !events?.length) return; - - const currentParams: UpdateParams = { - events, eventCount: events?.length || 0, - levels: currentLevels, star: starVisualization, - tags: showTagAnchors, tagType: selectedTagType, - disabledCount: disabledTags.size, - tagExpansion: tagExpansionDepth, theme: isDarkMode - }; - - // Detect changes - changedParams = detectChanges(lastUpdateParams, currentParams); - - if (changedParams.size === 0) { - debug("No parameter changes detected"); - return; - } - - // Determine update type - const updateType = detectUpdateType(changedParams); - logUpdateType(updateType, changedParams); // Production logging - - // Update last parameters immediately - lastUpdateParams = { ...currentParams }; - - // Route to appropriate update - if (updateType.kind === 'full') { - performUpdate(updateType); // Immediate - } else { - debouncedPerformUpdate(updateType); // Debounced - } -}); -``` - -### 6. Debouncing - -Intelligent debouncing prevents update storms: - -```typescript -const debouncedPerformUpdate = debounce(performUpdate, 150); - -function performUpdate(updateType: UpdateType) { - try { - switch (updateType.kind) { - case 'full': - updateGraph(); - break; - - case 'structural': - updateGraph(); // TODO: updateGraphStructure() - break; - - case 'visual': - if (updateType.params.has('star') || - updateType.params.has('disabledCount') || - updateType.params.has('theme')) { - updateVisualProperties(); - } else { - updateGraph(); // Fallback - } - break; - } - } catch (error) { - console.error("Error in performUpdate:", error); - errorMessage = `Error updating graph: ${error instanceof Error ? error.message : String(error)}`; - } -} -``` - -### 7. Theme Change Integration - -Theme changes now use the optimized path: - -```typescript -const themeObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === "class") { - const newIsDarkMode = document.body.classList.contains("dark"); - if (newIsDarkMode !== isDarkMode) { - isDarkMode = newIsDarkMode; - // The effect will detect this change and call updateVisualProperties() - } - } - }); -}); -``` - -### 8. Component-Level State - -Nodes and links are now persisted at component level: - -```typescript -// Graph data - persisted between updates -let nodes = $state([]); -let links = $state([]); -``` - -## Performance Monitoring - -Both update functions include timing: - -```typescript -const startTime = performance.now(); -// ... update logic ... -const updateTime = performance.now() - startTime; -debug(`Update completed in ${updateTime.toFixed(2)}ms`); -``` - -## Testing the Implementation - -### Manual Testing - -1. **Enable debug mode**: `const DEBUG = true;` -2. **Open browser console** -3. **Test scenarios**: - - Toggle star mode rapidly - - Click multiple tags in legend - - Switch theme - - Watch console for timing logs - -### Expected Console Output - -``` -[EventNetwork] Update type detected: visual Changed params: star -[EventNetwork] Performing visual update for params: ["star"] -[EventNetwork] Visual properties updated in 15.23ms -``` - -### Performance Validation - -- Visual updates should complete in <50ms -- No position jumps should occur -- Simulation should maintain momentum -- Rapid toggles should be batched - -## Utility Module Structure - -The change detection logic has been extracted to `/src/lib/utils/updateDetection.ts`: - -```typescript -export interface UpdateParams { - events: any; - eventCount: number; - levels: any; - star: boolean; - tags: boolean; - tagType: string; - disabledCount: number; - tagExpansion: number; - theme: boolean; -} - -export function detectChanges( - lastParams: UpdateParams, - currentParams: UpdateParams -): Set { - const changes = new Set(); - for (const [key, value] of Object.entries(currentParams)) { - if (value !== lastParams[key as keyof UpdateParams]) { - changes.add(key); - } - } - return changes; -} - -export function detectUpdateType(changes: Set): UpdateType { - if (changes.has('events') || changes.has('eventCount') || changes.has('levels')) { - return { kind: 'full', reason: 'Data or depth changed' }; - } - - if (changes.has('tags') || changes.has('tagType') || changes.has('tagExpansion')) { - return { - kind: 'structural', - reason: 'Graph structure changed', - params: changes - }; - } - - return { kind: 'visual', params: changes }; -} - -export function logUpdateType(updateType: UpdateType, changedParams: Set) { - if (process.env.NODE_ENV === 'production') { - console.log('[Visualization Update]', { - type: updateType.kind, - params: Array.from(changedParams), - timestamp: new Date().toISOString() - }); - } -} -``` - -## Migration Notes - -For developers updating existing code: - -1. **Import the utility module** for update detection -2. **Ensure nodes/links are at component level** -3. **Add theme to tracked parameters** -4. **Use the performUpdate function** for all updates -5. **Keep DEBUG = false in production** - -## Troubleshooting - -### Visual updates not working? -- Check that nodes/links are accessible -- Verify the parameter is in visual category -- Ensure simulation exists - -### Updates seem delayed? -- Check debounce timing (150ms default) -- Data updates bypass debouncing - -### Performance not improved? -- Verify DEBUG mode shows "visual update" -- Check browser console for errors -- Ensure not falling back to updateGraph() - ---- - -*Implementation guide by Claude Agent 3* -*Last updated: January 6, 2025* \ No newline at end of file diff --git a/docs/mini-projects/08-visualization-optimization-quick-reference.md b/docs/mini-projects/08-visualization-optimization-quick-reference.md deleted file mode 100644 index 252a115..0000000 --- a/docs/mini-projects/08-visualization-optimization-quick-reference.md +++ /dev/null @@ -1,124 +0,0 @@ -# Visualization Optimization Quick Reference - -## At a Glance - -The EventNetwork visualization now uses **shallow updates** for visual-only changes, improving performance by **90%+**. - -## What Changed? - -### Before -Every parameter change → Full graph recreation → 150-200ms - -### After -- **Visual changes** → Update existing elements → 10-30ms -- **Data changes** → Full recreation (as before) → 150-200ms - -## Parameter Categories - -### Visual Updates (Fast) ⚡ -- `starVisualization` - Star/standard layout -- `disabledTags` - Tag visibility in legend -- `isDarkMode` - Theme changes - -### Structural Updates (Medium) 🔧 -- `showTagAnchors` - Add/remove tag nodes -- `selectedTagType` - Change tag filter -- `tagExpansionDepth` - Expand relationships - -### Full Updates (Slow) 🐌 -- `events` - New data from relays -- `levelsToRender` - Depth changes -- `networkFetchLimit` - Fetch more events - -## Key Functions - -```typescript -// Detects what type of update is needed -detectUpdateType(changedParams) → UpdateType - -// Routes updates based on type -performUpdate(updateType) → void - -// Optimized visual updates -updateVisualProperties() → void - -// Full recreation (fallback) -updateGraph() → void -``` - -## Performance Targets - -| Update Type | Target | Actual | Status | -|------------|--------|--------|--------| -| Visual | <50ms | 10-30ms | ✅ | -| Debounce | 150ms | 150ms | ✅ | -| Position Preservation | Yes | Yes | ✅ | - -## Debug Mode - -```typescript -const DEBUG = true; // Line 52 - Shows timing in console -``` - -## Common Patterns - -### Adding a New Visual Parameter - -1. Add to `UpdateParams` interface -2. Track in `lastUpdateParams` -3. Handle in `updateVisualProperties()` -4. Add to visual check in `performUpdate()` - -### Testing Performance - -```javascript -// Browser console -window.performance.mark('start'); -// Toggle parameter -window.performance.mark('end'); -window.performance.measure('update', 'start', 'end'); -``` - -## Troubleshooting - -**Updates seem slow?** -- Check console for update type (should be "visual") -- Verify parameter is in correct category - -**Position jumps?** -- Ensure using `updateVisualProperties()` not `updateGraph()` -- Check nodes/links are persisted - -**Debouncing not working?** -- Visual updates have 150ms delay -- Data updates are immediate (no delay) - -## Architecture Diagram - -``` -User Action - ↓ -Parameter Change Detection - ↓ -Categorize Update Type - ↓ -┌─────────────┬──────────────┬─────────────┐ -│ Full │ Structural │ Visual │ -│ (Immediate)│ (Debounced) │ (Debounced) │ -└──────┬──────┴───────┬──────┴──────┬──────┘ - ↓ ↓ ↓ - updateGraph() updateGraph() updateVisualProperties() - (recreate all) (TODO: partial) (modify existing) -``` - -## Next Steps - -- [ ] Implement `updateGraphStructure()` for partial updates -- [ ] Add hover state support -- [ ] Performance monitoring dashboard -- [ ] Make debounce configurable - ---- - -*Quick reference by Claude Agent 3* -*For full details see: 08-visualization-optimization-implementation.md* \ No newline at end of file diff --git a/docs/mini-projects/08-visualization-optimization-summary.md b/docs/mini-projects/08-visualization-optimization-summary.md deleted file mode 100644 index 5cd988c..0000000 --- a/docs/mini-projects/08-visualization-optimization-summary.md +++ /dev/null @@ -1,168 +0,0 @@ -# Visualization Performance Optimization Summary - -**Date**: January 6, 2025 -**Project**: gc-alexandria Event Network Visualization -**Coordination**: Claude Agent 3 (Master Coordinator) - -## Executive Summary - -Successfully implemented a shallow copy update mechanism that reduces visualization update times by 90%+ for visual-only parameter changes. The optimization avoids full graph recreation when only visual properties change, resulting in smoother user experience and better performance. - -## Problem Statement - -The visualization component (`/src/lib/navigator/EventNetwork/index.svelte`) was recreating the entire D3.js force simulation graph on every parameter change, including visual-only changes like: -- Star visualization mode toggle -- Tag visibility toggles -- Theme changes - -This caused: -- 150-200ms delays for simple visual updates -- Position jumps as nodes were recreated -- Loss of simulation momentum -- Poor user experience with rapid interactions - -## Solution Architecture - -### Three-Tier Update System - -Implemented a discriminated union type system to categorize updates: - -```typescript -type UpdateType = - | { kind: 'full'; reason: string } - | { kind: 'structural'; reason: string; params: Set } - | { kind: 'visual'; params: Set }; -``` - -### Update Categories - -1. **Full Updates** (Data changes): - - New events from relays - - Depth level changes - - Requires complete graph recreation - -2. **Structural Updates** (Graph structure changes): - - Tag anchor additions/removals - - Tag type changes - - Requires partial graph update (future work) - -3. **Visual Updates** (Appearance only): - - Star mode toggle - - Tag visibility - - Theme changes - - Uses optimized `updateVisualProperties()` function - -### Key Implementation Details - -1. **Parameter Change Detection**: - - Tracks current vs previous parameter values - - Detects exactly what changed - - Routes to appropriate update handler - -2. **Visual Update Optimization**: - - Modifies existing DOM elements in-place - - Updates simulation forces without recreation - - Preserves node positions and momentum - - Uses gentle simulation restart (alpha 0.3) - -3. **Intelligent Debouncing**: - - 150ms delay for visual/structural updates - - Immediate updates for data changes - - Prevents update storms during rapid interactions - -## Performance Results - -### Metrics - -| Update Type | Before | After | Improvement | -|------------|--------|-------|-------------| -| Star Mode Toggle | 150-200ms | 10-30ms | 90% faster | -| Tag Visibility | 150-200ms | 5-15ms | 93% faster | -| Theme Change | 150-200ms | 10-20ms | 92% faster | - -### Benefits - -- ✅ No position jumps -- ✅ Smooth transitions -- ✅ Maintains simulation state -- ✅ Handles rapid parameter changes -- ✅ Reduced memory allocation - -## Code Architecture - -### Layer Separation Model - -``` -┌─────────────────────────────┐ -│ Data Layer │ ← Nostr events -├─────────────────────────────┤ -│ Graph Model Layer │ ← Nodes and links -├─────────────────────────────┤ -│ Simulation Layer │ ← Force physics -├─────────────────────────────┤ -│ Rendering Layer │ ← SVG/DOM -└─────────────────────────────┘ -``` - -This architecture enables updates at any layer without affecting layers above. - -## Implementation Timeline - -1. **Analysis Phase** (Agent 1): - - Identified full recreation issue - - Documented update triggers - - Created optimization proposal - -2. **Implementation Phase** (Agent 1): - - Added update type detection - - Created `updateVisualProperties()` - - Integrated parameter tracking - - Added debouncing - -3. **Testing Phase** (Agent 2): - - Created 50+ test cases - - Validated performance improvements - - Tested edge cases - -## Key Files Modified - -- `/src/lib/navigator/EventNetwork/index.svelte` - Main visualization component -- Added ~200 lines of optimization code -- Preserved backward compatibility - -## Testing Coverage - -Agent 2 created comprehensive test coverage: -- **E2E Tests**: Collapsible UI, tag interactions -- **Unit Tests**: Update detection, deduplication -- **Integration Tests**: Display limits, reactivity paths -- **Performance Tests**: Timing validation, memory usage - -## Future Enhancements - -1. **Structural Updates** - Implement `updateGraphStructure()` for partial graph updates -2. **Change Detection Extraction** - Move to utility module -3. **Performance Dashboard** - Real-time monitoring -4. **Additional Visual Properties** - Hover states, animations - -## Lessons Learned - -1. **Profiling First** - Understanding the problem through analysis was crucial -2. **Incremental Approach** - Starting with visual updates proved the concept -3. **Layer Separation** - Clean architecture enables targeted optimizations -4. **Debouncing Matters** - Critical for handling rapid user interactions - -## Team Contributions - -- **Agent 1 (Visualization)**: Analysis, implementation, documentation -- **Agent 2 (Testing)**: Test infrastructure, validation, performance baselines -- **Agent 3 (Coordination)**: Architecture guidance, code reviews, documentation - -## Conclusion - -The shallow copy optimization successfully addresses the performance issues while maintaining code quality and user experience. The 90%+ improvement in update times creates a noticeably smoother interaction, especially for users rapidly toggling visualization parameters. - ---- - -*Documentation created by Claude Agent 3 (Master Coordinator)* -*Last updated: January 6, 2025* \ No newline at end of file diff --git a/src/lib/components/EventTypeConfig.svelte b/src/lib/components/EventTypeConfig.svelte index 662b28b..3b5c3ea 100644 --- a/src/lib/components/EventTypeConfig.svelte +++ b/src/lib/components/EventTypeConfig.svelte @@ -102,7 +102,7 @@
{#each $visualizationConfig.eventConfigs as config} {@const isLoaded = (eventCounts[config.kind] || 0) > 0} - {@const isDisabled = $visualizationConfig.disabledKinds?.includes(config.kind) || false} + {@const isDisabled = config.enabled === false} {@const color = getEventKindColor(config.kind)} {@const borderColor = isLoaded ? 'border-green-500' : 'border-red-500'}
diff --git a/src/lib/navigator/EventNetwork/Legend.svelte b/src/lib/navigator/EventNetwork/Legend.svelte index 4889c4e..0e53c56 100644 --- a/src/lib/navigator/EventNetwork/Legend.svelte +++ b/src/lib/navigator/EventNetwork/Legend.svelte @@ -1,5 +1,4 @@
@@ -102,24 +91,27 @@
{#if visualSettingsExpanded} -
- -

- Toggle between star clusters (on) and linear sequence (off) - visualization -

+
+
+ +

+ Toggle between star clusters (on) and linear sequence (off) + visualization +

+
+
{/if} diff --git a/src/lib/navigator/EventNetwork/index.svelte b/src/lib/navigator/EventNetwork/index.svelte index aebb691..2879f97 100644 --- a/src/lib/navigator/EventNetwork/index.svelte +++ b/src/lib/navigator/EventNetwork/index.svelte @@ -74,7 +74,6 @@ onupdate, onclear = () => {}, onTagExpansionChange, - onFetchMissing = () => {}, profileStats = { totalFetched: 0, displayLimit: 50 }, allEventCounts = {} } = $props<{ @@ -84,7 +83,6 @@ onupdate: () => void; onclear?: () => void; onTagExpansionChange?: (depth: number, tags: string[]) => void; - onFetchMissing?: (ids: string[]) => void; profileStats?: { totalFetched: number; displayLimit: number }; allEventCounts?: { [kind: number]: number }; }>(); @@ -134,7 +132,6 @@ let showTagAnchors = $state(false); let selectedTagType = $state("t"); // Default to hashtags let tagAnchorInfo = $state([]); - let tagExpansionDepth = $state(0); // Default to no expansion // Store initial state to detect if component is being recreated let componentId = Math.random(); @@ -171,21 +168,6 @@ let displayedPersonCount = $state(0); let hasInitializedPersons = $state(false); - // Debug function - call from browser console: window.debugTagAnchors() - if (typeof window !== "undefined") { - window.debugTagAnchors = () => { - console.log("=== TAG ANCHOR DEBUG INFO ==="); - console.log("Tag Anchor Info:", tagAnchorInfo); - console.log("Show Tag Anchors:", showTagAnchors); - console.log("Selected Tag Type:", selectedTagType); - const tagNodes = nodes.filter((n) => n.isTagAnchor); - console.log("Tag Anchor Nodes:", tagNodes); - console.log("Tag Types Found:", [ - ...new Set(tagNodes.map((n) => n.tagType)), - ]); - return tagAnchorInfo; - }; - } // Update dimensions when container changes $effect(() => { @@ -1001,7 +983,6 @@ }); // Track previous values to avoid unnecessary calls - let previousDepth = $state(0); let previousTagType = $state(selectedTagType); let isInitialized = $state(false); @@ -1020,12 +1001,10 @@ if (!isInitialized || !onTagExpansionChange) return; // Check if we need to trigger expansion - const depthChanged = tagExpansionDepth !== previousDepth; const tagTypeChanged = selectedTagType !== previousTagType; - const shouldExpand = showTagAnchors && (depthChanged || tagTypeChanged); + const shouldExpand = showTagAnchors && tagTypeChanged; if (shouldExpand) { - previousDepth = tagExpansionDepth; previousTagType = selectedTagType; // Extract unique tags from current events @@ -1038,14 +1017,12 @@ }); debug("Tag expansion requested", { - depth: tagExpansionDepth, tagType: selectedTagType, tags: Array.from(tags), - depthChanged, tagTypeChanged }); - onTagExpansionChange(tagExpansionDepth, Array.from(tags)); + onTagExpansionChange(0, Array.from(tags)); } }); @@ -1221,7 +1198,6 @@ {autoDisabledTags} bind:showTagAnchors bind:selectedTagType - bind:tagExpansionDepth onTagSettingsChange={() => { // Trigger graph update when tag settings change if (svg && events?.length) { @@ -1250,7 +1226,6 @@ {totalCount} {onupdate} {onclear} - {onFetchMissing} bind:starVisualization eventCounts={allEventCounts} {profileStats} diff --git a/src/lib/navigator/EventNetwork/utils/common.ts b/src/lib/navigator/EventNetwork/utils/common.ts new file mode 100644 index 0000000..f8c0bef --- /dev/null +++ b/src/lib/navigator/EventNetwork/utils/common.ts @@ -0,0 +1,41 @@ +/** + * Common utilities shared across network builders + */ + +/** + * Seeded random number generator for deterministic layouts + */ +export class SeededRandom { + private seed: number; + + constructor(seed: number) { + this.seed = seed; + } + + next(): number { + const x = Math.sin(this.seed++) * 10000; + return x - Math.floor(x); + } + + nextFloat(min: number, max: number): number { + return min + this.next() * (max - min); + } + + nextInt(min: number, max: number): number { + return Math.floor(this.nextFloat(min, max + 1)); + } +} + +/** + * Creates a debug function with a prefix + * @param prefix - The prefix to add to all debug messages + * @returns A debug function that can be toggled on/off + */ +export function createDebugFunction(prefix: string) { + const DEBUG = false; + return function debug(...args: any[]) { + if (DEBUG) { + console.log(`[${prefix}]`, ...args); + } + }; +} \ No newline at end of file diff --git a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts index 17d65bc..d74ba1d 100644 --- a/src/lib/navigator/EventNetwork/utils/forceSimulation.ts +++ b/src/lib/navigator/EventNetwork/utils/forceSimulation.ts @@ -7,20 +7,14 @@ import type { NetworkNode, NetworkLink } from "../types"; import * as d3 from "d3"; +import { createDebugFunction } from "./common"; // Configuration -const DEBUG = false; // Set to true to enable debug logging const GRAVITY_STRENGTH = 0.05; // Strength of global gravity const CONNECTED_GRAVITY_STRENGTH = 0.3; // Strength of gravity between connected nodes -/** - * Debug logging function that only logs when DEBUG is true - */ -function debug(...args: any[]) { - if (DEBUG) { - console.log("[ForceSimulation]", ...args); - } -} +// Debug function +const debug = createDebugFunction("ForceSimulation"); /** * Type definition for D3 force simulation diff --git a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts index c14fe65..9983edd 100644 --- a/src/lib/navigator/EventNetwork/utils/networkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/networkBuilder.ts @@ -11,20 +11,14 @@ import { nip19 } from "nostr-tools"; import { standardRelays } from "$lib/consts"; import { getMatchingTags } from '$lib/utils/nostrUtils'; import { getDisplayNameSync } from '$lib/utils/profileCache'; +import { createDebugFunction } from "./common"; // Configuration -const DEBUG = false; // Set to true to enable debug logging const INDEX_EVENT_KIND = 30040; const CONTENT_EVENT_KIND = 30041; -/** - * Debug logging function that only logs when DEBUG is true - */ -function debug(...args: any[]) { - if (DEBUG) { - console.log("[NetworkBuilder]", ...args); - } -} +// Debug function +const debug = createDebugFunction("NetworkBuilder"); /** * Creates a NetworkNode from an NDKEvent diff --git a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts index 5d9bfb9..fc60288 100644 --- a/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts @@ -7,26 +7,15 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink } from "../types"; import { getDisplayNameSync } from "$lib/utils/profileCache"; +import { SeededRandom, createDebugFunction } from "./common"; const PERSON_ANCHOR_RADIUS = 15; const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000; const MAX_PERSON_NODES = 20; // Default limit for person nodes -/** - * Simple seeded random number generator - */ -class SeededRandom { - private seed: number; - - constructor(seed: number) { - this.seed = seed; - } +// Debug function +const debug = createDebugFunction("PersonNetworkBuilder"); - next(): number { - this.seed = (this.seed * 9301 + 49297) % 233280; - return this.seed / 233280; - } -} /** * Creates a deterministic seed from a string @@ -58,12 +47,11 @@ export function extractUniquePersons( // Map of pubkey -> PersonConnection const personMap = new Map(); - console.log(`[PersonBuilder] Extracting persons from ${events.length} events`); + debug("Extracting unique persons", { eventCount: events.length, followListCount: followListEvents?.length || 0 }); // First collect pubkeys from follow list events const followListPubkeys = new Set(); if (followListEvents && followListEvents.length > 0) { - console.log(`[PersonBuilder] Processing ${followListEvents.length} follow list events`); followListEvents.forEach((event) => { // Follow list author if (event.pubkey) { @@ -113,8 +101,7 @@ export function extractUniquePersons( } }); - console.log(`[PersonBuilder] Found ${personMap.size} unique persons`); - console.log(`[PersonBuilder] ${followListPubkeys.size} are from follow lists`); + debug("Extracted persons", { personCount: personMap.size }); return personMap; } @@ -171,8 +158,14 @@ export function createPersonAnchorNodes( const limitedPersons = eligiblePersons.slice(0, limit); // Create nodes for the limited set - limitedPersons.forEach(({ pubkey, connection, connectedEventIds }) => { + debug("Creating person anchor nodes", { + eligibleCount: eligiblePersons.length, + limitedCount: limitedPersons.length, + showSignedBy, + showReferenced + }); + limitedPersons.forEach(({ pubkey, connection, connectedEventIds }) => { // Create seeded random generator for consistent positioning const rng = new SeededRandom(createSeed(pubkey)); @@ -207,6 +200,8 @@ export function createPersonAnchorNodes( anchorNodes.push(anchorNode); }); + debug("Created person anchor nodes", { count: anchorNodes.length, totalEligible: eligiblePersons.length }); + return { nodes: anchorNodes, totalCount: eligiblePersons.length @@ -226,6 +221,8 @@ export function createPersonLinks( nodes: NetworkNode[], personMap: Map ): PersonLink[] { + debug("Creating person links", { anchorCount: personAnchors.length, nodeCount: nodes.length }); + const links: PersonLink[] = []; const nodeMap = new Map(nodes.map((n) => [n.id, n])); @@ -256,6 +253,7 @@ export function createPersonLinks( }); }); + debug("Created person links", { linkCount: links.length }); return links; } diff --git a/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts index 7c0a595..985e607 100644 --- a/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts @@ -11,20 +11,14 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types"; import { getMatchingTags } from '$lib/utils/nostrUtils'; import { createNetworkNode, createEventMap, extractEventIdFromATag, getEventColor } from './networkBuilder'; +import { createDebugFunction } from './common'; // Configuration -const DEBUG = false; const INDEX_EVENT_KIND = 30040; const CONTENT_EVENT_KIND = 30041; -/** - * Debug logging function - */ -function debug(...args: any[]) { - if (DEBUG) { - console.log("[StarNetworkBuilder]", ...args); - } -} +// Debug function +const debug = createDebugFunction("StarNetworkBuilder"); /** * Represents a star network with a central index node and peripheral content nodes diff --git a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts index 6562a88..79e2330 100644 --- a/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts +++ b/src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts @@ -8,29 +8,16 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NetworkNode, NetworkLink, GraphData } from "../types"; import { getDisplayNameSync } from "$lib/utils/profileCache"; +import { SeededRandom, createDebugFunction } from "./common"; // Configuration const TAG_ANCHOR_RADIUS = 15; // TODO: Move this to settings panel for user control const TAG_ANCHOR_PLACEMENT_RADIUS = 1250; // Radius from center within which to randomly place tag anchors -/** - * Simple seeded random number generator (using a Linear Congruential Generator) - * This ensures consistent positioning for the same tag values across sessions - */ -class SeededRandom { - private seed: number; - - constructor(seed: number) { - this.seed = seed; - } +// Debug function +const debug = createDebugFunction("TagNetworkBuilder"); - // Generate next random number between 0 and 1 - next(): number { - this.seed = (this.seed * 9301 + 49297) % 233280; - return this.seed / 233280; - } -} /** * Creates a deterministic seed from a string @@ -76,8 +63,7 @@ export function extractUniqueTagsForType( ): Map> { // Map of tagValue -> Set of event IDs const tagMap = new Map>(); - - console.log(`[TagBuilder] Extracting tags of type: ${tagType} from ${events.length} events`); + debug("Extracting unique tags for type", { tagType, eventCount: events.length }); events.forEach((event) => { if (!event.tags || !event.id) return; @@ -98,7 +84,7 @@ export function extractUniqueTagsForType( }); }); - console.log(`[TagBuilder] Found ${tagMap.size} unique tags of type ${tagType}:`, Array.from(tagMap.keys())); + debug("Extracted tags", { tagCount: tagMap.size }); return tagMap; } @@ -114,6 +100,8 @@ export function createTagAnchorNodes( ): NetworkNode[] { const anchorNodes: NetworkNode[] = []; + debug("Creating tag anchor nodes", { tagType, tagCount: tagMap.size }); + // Calculate positions for tag anchors randomly within radius // Show all tags regardless of how many events they appear in const minEventCount = 1; @@ -173,6 +161,7 @@ export function createTagAnchorNodes( anchorNodes.push(anchorNode); }); + debug("Created tag anchor nodes", { count: anchorNodes.length }); return anchorNodes; } @@ -183,6 +172,8 @@ export function createTagLinks( tagAnchors: NetworkNode[], nodes: NetworkNode[], ): NetworkLink[] { + debug("Creating tag links", { anchorCount: tagAnchors.length, nodeCount: nodes.length }); + const links: NetworkLink[] = []; const nodeMap = new Map(nodes.map((n) => [n.id, n])); @@ -201,6 +192,7 @@ export function createTagLinks( }); }); + debug("Created tag links", { linkCount: links.length }); return links; } @@ -215,6 +207,8 @@ export function enhanceGraphWithTags( height: number, displayLimit?: number, ): GraphData { + debug("Enhancing graph with tags", { tagType, displayLimit }); + // Extract unique tags for the specified type const tagMap = extractUniqueTagsForType(events, tagType); @@ -223,7 +217,6 @@ export function enhanceGraphWithTags( // Apply display limit if provided if (displayLimit && displayLimit > 0 && tagAnchors.length > displayLimit) { - console.log(`[TagBuilder] Limiting display to ${displayLimit} tag anchors out of ${tagAnchors.length}`); // Sort by connection count (already done in createTagAnchorNodes) // and take only the top ones up to the limit tagAnchors = tagAnchors.slice(0, displayLimit); @@ -266,6 +259,8 @@ export function createTagGravityForce( } }); + debug("Creating tag gravity force"); + // Custom force function function force(alpha: number) { nodes.forEach((node) => { diff --git a/src/lib/stores/displayLimits.ts b/src/lib/stores/displayLimits.ts deleted file mode 100644 index a8103b6..0000000 --- a/src/lib/stores/displayLimits.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { writable } from 'svelte/store'; - -export interface DisplayLimits { - max30040: number; // -1 for unlimited - max30041: number; // -1 for unlimited - fetchIfNotFound: boolean; -} - -// Create the store with default values -export const displayLimits = writable({ - max30040: -1, // Show all publication indices by default - max30041: -1, // Show all content by default - fetchIfNotFound: false // Don't fetch missing events by default -}); - -// Helper to check if limits are active -export function hasActiveLimits(limits: DisplayLimits): boolean { - return limits.max30040 !== -1 || limits.max30041 !== -1; -} \ No newline at end of file diff --git a/src/lib/stores/visualizationConfig.ts b/src/lib/stores/visualizationConfig.ts index 6c7e8e9..b689fc5 100644 --- a/src/lib/stores/visualizationConfig.ts +++ b/src/lib/stores/visualizationConfig.ts @@ -3,6 +3,7 @@ import { writable, derived, get } from "svelte/store"; export interface EventKindConfig { kind: number; limit: number; + enabled?: boolean; // Whether this kind is enabled for display nestedLevels?: number; // Only for kind 30040 depth?: number; // Only for kind 3 (follow lists) showAll?: boolean; // Only for content kinds (30041, 30818) - show all loaded content instead of limit @@ -14,50 +15,26 @@ export interface VisualizationConfig { // Graph traversal searchThroughFetched: boolean; - - // Append mode - add new events to existing graph instead of replacing - appendMode?: boolean; - - // Legacy properties for backward compatibility - allowedKinds?: number[]; - disabledKinds?: number[]; - allowFreeEvents?: boolean; - maxPublicationIndices?: number; - maxEventsPerIndex?: number; } // Default configurations for common event kinds const DEFAULT_EVENT_CONFIGS: EventKindConfig[] = [ - { kind: 0, limit: 5 }, // Metadata events (profiles) - controls how many profiles to display - { kind: 3, limit: 0, depth: 0 }, // Follow lists - limit 0 = don't fetch, >0 = fetch follow lists - { kind: 30040, limit: 20, nestedLevels: 1 }, - { kind: 30041, limit: 20 }, - { kind: 30818, limit: 20 }, + { kind: 0, limit: 5, enabled: false }, // Metadata events (profiles) - controls how many profiles to display + { kind: 3, limit: 0, depth: 0, enabled: false }, // Follow lists - limit 0 = don't fetch, >0 = fetch follow lists + { kind: 30040, limit: 20, nestedLevels: 1, enabled: true }, + { kind: 30041, limit: 20, enabled: false }, + { kind: 30818, limit: 20, enabled: false }, ]; function createVisualizationConfig() { - // Initialize with both new and legacy properties const initialConfig: VisualizationConfig = { eventConfigs: DEFAULT_EVENT_CONFIGS, searchThroughFetched: true, - appendMode: false, - // Legacy properties - allowedKinds: DEFAULT_EVENT_CONFIGS.map((ec) => ec.kind), - disabledKinds: [30041, 30818, 3, 0], // Kind 0 not disabled so it shows as green when profiles are fetched - allowFreeEvents: false, - maxPublicationIndices: -1, - maxEventsPerIndex: -1, }; const { subscribe, set, update } = writable(initialConfig); - // Helper to sync legacy properties with eventConfigs - const syncLegacyProperties = (config: VisualizationConfig) => { - config.allowedKinds = config.eventConfigs.map((ec) => ec.kind); - return config; - }; - return { subscribe, update, @@ -71,7 +48,7 @@ function createVisualizationConfig() { return config; } - const newConfig: EventKindConfig = { kind, limit }; + const newConfig: EventKindConfig = { kind, limit, enabled: true }; // Add nestedLevels for 30040 if (kind === 30040) { newConfig.nestedLevels = 1; @@ -85,20 +62,7 @@ function createVisualizationConfig() { ...config, eventConfigs: [...config.eventConfigs, newConfig], }; - return syncLegacyProperties(updated); - }), - - // Legacy method for backward compatibility - addKind: (kind: number) => - update((config) => { - if (config.eventConfigs.some((ec) => ec.kind === kind)) { - return config; - } - const updated = { - ...config, - eventConfigs: [...config.eventConfigs, { kind, limit: 10 }], - }; - return syncLegacyProperties(updated); + return updated; }), // Remove an event kind @@ -108,17 +72,7 @@ function createVisualizationConfig() { ...config, eventConfigs: config.eventConfigs.filter((ec) => ec.kind !== kind), }; - return syncLegacyProperties(updated); - }), - - // Legacy method for backward compatibility - removeKind: (kind: number) => - update((config) => { - const updated = { - ...config, - eventConfigs: config.eventConfigs.filter((ec) => ec.kind !== kind), - }; - return syncLegacyProperties(updated); + return updated; }), // Update limit for a specific kind @@ -172,48 +126,13 @@ function createVisualizationConfig() { searchThroughFetched: !config.searchThroughFetched, })), - toggleAppendMode: () => - update((config) => ({ - ...config, - appendMode: !config.appendMode, - })), - - // Legacy methods for backward compatibility + // Toggle enabled state for a specific kind toggleKind: (kind: number) => - update((config) => { - const isDisabled = config.disabledKinds?.includes(kind) || false; - if (isDisabled) { - // Re-enable it - return { - ...config, - disabledKinds: - config.disabledKinds?.filter((k) => k !== kind) || [], - }; - } else { - // Disable it - return { - ...config, - disabledKinds: [...(config.disabledKinds || []), kind], - }; - } - }), - - toggleFreeEvents: () => - update((config) => ({ - ...config, - allowFreeEvents: !config.allowFreeEvents, - })), - - setMaxPublicationIndices: (max: number) => - update((config) => ({ - ...config, - maxPublicationIndices: max, - })), - - setMaxEventsPerIndex: (max: number) => update((config) => ({ ...config, - maxEventsPerIndex: max, + eventConfigs: config.eventConfigs.map((ec) => + ec.kind === kind ? { ...ec, enabled: !ec.enabled } : ec, + ), })), }; } @@ -222,22 +141,16 @@ export const visualizationConfig = createVisualizationConfig(); // Helper to get all enabled event kinds export const enabledEventKinds = derived(visualizationConfig, ($config) => - $config.eventConfigs.map((ec) => ec.kind), + $config.eventConfigs + .filter((ec) => ec.enabled !== false) + .map((ec) => ec.kind), ); // Helper to check if a kind is enabled export const isKindEnabled = derived( - visualizationConfig, - ($config) => (kind: number) => - $config.eventConfigs.some((ec) => ec.kind === kind), -); - -// Legacy helper for backward compatibility -export const isKindAllowed = derived( visualizationConfig, ($config) => (kind: number) => { - const inEventConfigs = $config.eventConfigs.some((ec) => ec.kind === kind); - const notDisabled = !($config.disabledKinds?.includes(kind) || false); - return inEventConfigs && notDisabled; + const eventConfig = $config.eventConfigs.find((ec) => ec.kind === kind); + return eventConfig ? eventConfig.enabled !== false : false; }, ); diff --git a/src/lib/utils/displayLimits.ts b/src/lib/utils/displayLimits.ts index ade3bde..e41b64f 100644 --- a/src/lib/utils/displayLimits.ts +++ b/src/lib/utils/displayLimits.ts @@ -1,34 +1,28 @@ import type { NDKEvent } from '@nostr-dev-kit/ndk'; -import type { DisplayLimits } from '$lib/stores/displayLimits'; import type { VisualizationConfig } from '$lib/stores/visualizationConfig'; /** - * Filters events based on display limits and allowed kinds + * Filters events based on visualization configuration * @param events - All available events - * @param limits - Display limit settings - * @param config - Visualization configuration (optional) + * @param config - Visualization configuration * @returns Filtered events that should be displayed */ -export function filterByDisplayLimits(events: NDKEvent[], limits: DisplayLimits, config?: VisualizationConfig): NDKEvent[] { +export function filterByDisplayLimits(events: NDKEvent[], config: VisualizationConfig): NDKEvent[] { const result: NDKEvent[] = []; const kindCounts = new Map(); for (const event of events) { - // First check if the event kind is allowed and not disabled - if (config && event.kind !== undefined) { - if (!config.allowedKinds.includes(event.kind)) { - continue; // Skip events with disallowed kinds - } - if (config.disabledKinds.includes(event.kind)) { - continue; // Skip temporarily disabled kinds - } - } - const kind = event.kind; if (kind === undefined) continue; - // Get the limit for this event kind from the config - const eventConfig = config?.eventConfigs.find(ec => ec.kind === kind); + // Get the config for this event kind + const eventConfig = config.eventConfigs.find(ec => ec.kind === kind); + + // Skip if the kind is disabled + if (eventConfig && eventConfig.enabled === false) { + continue; + } + const limit = eventConfig?.limit; // Special handling for content kinds (30041, 30818) with showAll option @@ -105,37 +99,3 @@ export function detectMissingEvents(events: NDKEvent[], existingIds: Set return missing; } -/** - * Groups events by kind for easier counting and display - */ -export function groupEventsByKind(events: NDKEvent[]): Map { - const groups = new Map(); - - for (const event of events) { - const kind = event.kind; - if (kind !== undefined) { - if (!groups.has(kind)) { - groups.set(kind, []); - } - groups.get(kind)!.push(event); - } - } - - return groups; -} - -/** - * Counts events by kind - */ -export function countEventsByKind(events: NDKEvent[]): Map { - const counts = new Map(); - - for (const event of events) { - const kind = event.kind; - if (kind !== undefined) { - counts.set(kind, (counts.get(kind) || 0) + 1); - } - } - - return counts; -} \ No newline at end of file diff --git a/src/lib/utils/eventColors.ts b/src/lib/utils/eventColors.ts index d820a44..77004cf 100644 --- a/src/lib/utils/eventColors.ts +++ b/src/lib/utils/eventColors.ts @@ -80,13 +80,3 @@ export function getEventKindName(kind: number): string { return kindNames[kind] || `Kind ${kind}`; } -/** - * Get the short label for an event kind (for node display) - * @param kind - The event kind number - * @returns Short label (usually just the kind number) - */ -export function getEventKindLabel(kind: number): string { - // For now, just return the kind number - // Could be extended to return short codes if needed - return kind.toString(); -} \ No newline at end of file diff --git a/src/routes/visualize/+page.svelte b/src/routes/visualize/+page.svelte index eb2c0fc..7a521f3 100644 --- a/src/routes/visualize/+page.svelte +++ b/src/routes/visualize/+page.svelte @@ -12,7 +12,6 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; import { filterValidIndexEvents } from "$lib/utils"; import { networkFetchLimit } from "$lib/state"; - import { displayLimits } from "$lib/stores/displayLimits"; import { visualizationConfig, type EventKindConfig } from "$lib/stores/visualizationConfig"; import { filterByDisplayLimits, detectMissingEvents } from "$lib/utils/displayLimits"; import type { PageData } from './$types'; @@ -43,7 +42,6 @@ let loading = $state(true); let error = $state(null); let showSettings = $state(false); - let tagExpansionDepth = $state(0); let baseEvents = $state([]); // Store original events before expansion let missingEventIds = $state(new Set()); // Track missing referenced events let loadingEventKinds = $state>([]); // Track what kinds are being loaded @@ -486,27 +484,9 @@ finalEventMap.set(event.id, event); }); - // Handle append mode - if ($visualizationConfig.appendMode && allEvents.length > 0) { - // Merge existing events with new events - const existingEventMap = new Map(allEvents.map(e => [e.id, e])); - - // Add new events to existing map (new events override old ones) - finalEventMap.forEach((event, id) => { - existingEventMap.set(id, event); - }); - - allEvents = Array.from(existingEventMap.values()); - - // Note: followListEvents are already accumulated in fetchFollowLists - } else { - // Replace mode (default) - allEvents = Array.from(finalEventMap.values()); - // Clear follow lists in replace mode - if (!$visualizationConfig.appendMode) { - followListEvents = []; - } - } + // Replace mode (always replace, no append mode) + allEvents = Array.from(finalEventMap.values()); + followListEvents = []; baseEvents = [...allEvents]; // Store base events for tag expansion @@ -571,7 +551,7 @@ } // Step 7: Apply display limits - events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); + events = filterByDisplayLimits(allEvents, $visualizationConfig); // Step 8: Detect missing events const eventIds = new Set(allEvents.map(e => e.id)); @@ -580,7 +560,6 @@ debug("Total events fetched:", allEvents.length); debug("Events displayed:", events.length); debug("Missing event IDs:", missingEventIds.size); - debug("Display limits:", $displayLimits); debug("About to set loading to false"); debug("Current loading state:", loading); } catch (e) { @@ -604,7 +583,7 @@ if (depth === 0 || tags.length === 0) { // Reset to base events only allEvents = [...baseEvents]; - events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); + events = filterByDisplayLimits(allEvents, $visualizationConfig); return; } @@ -789,7 +768,7 @@ } // Apply display limits - events = filterByDisplayLimits(allEvents, $displayLimits); + events = filterByDisplayLimits(allEvents, $visualizationConfig); // Update missing events detection const eventIds = new Set(allEvents.map(e => e.id)); @@ -811,76 +790,12 @@ } } - /** - * Dynamically fetches missing events when "fetch if not found" is enabled - */ - async function fetchMissingEvents(missingIds: string[]) { - if (!$displayLimits.fetchIfNotFound || missingIds.length === 0) { - return; - } - - debug("Fetching missing events:", missingIds); - debug("Current loading state:", loading); - - try { - // Fetch by event IDs and d-tags - const fetchedEvents = await $ndkInstance.fetchEvents({ - kinds: [...[INDEX_EVENT_KIND], ...CONTENT_EVENT_KINDS], - "#d": missingIds, // For parameterized replaceable events - }); - - if (fetchedEvents.size === 0) { - // Try fetching by IDs directly - const eventsByIds = await $ndkInstance.fetchEvents({ - ids: missingIds - }); - // Add events from the second fetch to the first set - eventsByIds.forEach(e => fetchedEvents.add(e)); - } - - if (fetchedEvents.size > 0) { - debug(`Fetched ${fetchedEvents.size} missing events`); - - // Fetch profiles for the new events - const newEvents = Array.from(fetchedEvents); - const newPubkeys = extractPubkeysFromEvents(newEvents); - let newProfileEvents: NDKEvent[] = []; - - if (newPubkeys.size > 0 && $visualizationConfig.eventConfigs.some(ec => ec.kind === 0 && !$visualizationConfig.disabledKinds?.includes(0))) { - debug("Fetching profiles for", newPubkeys.size, "pubkeys from missing events"); - profileLoadingProgress = { current: 0, total: newPubkeys.size }; - newProfileEvents = await batchFetchProfiles(Array.from(newPubkeys), (fetched, total) => { - profileLoadingProgress = { current: fetched, total }; - }); - profileLoadingProgress = null; - - // Update profile stats - profileStats = { - totalFetched: profileStats.totalFetched + newPubkeys.size, - displayLimit: profileStats.displayLimit - }; - } - - // Add to all events - allEvents = [...allEvents, ...newEvents, ...newProfileEvents]; - - // Re-apply display limits - events = filterByDisplayLimits(allEvents, $displayLimits); - - // Update missing events list - const eventIds = new Set(allEvents.map(e => e.id)); - missingEventIds = detectMissingEvents(events, eventIds); - } - } catch (e) { - console.error("Error fetching missing events:", e); - } - } // React to display limit and allowed kinds changes $effect(() => { - debug("Effect triggered: allEvents.length =", allEvents.length, "displayLimits =", $displayLimits, "allowedKinds =", $visualizationConfig.allowedKinds); + debug("Effect triggered: allEvents.length =", allEvents.length, "allowedKinds =", $visualizationConfig.allowedKinds); if (allEvents.length > 0) { - const newEvents = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); + const newEvents = filterByDisplayLimits(allEvents, $visualizationConfig); // Only update if actually different to avoid infinite loops if (newEvents.length !== events.length) { @@ -893,12 +808,6 @@ debug("Effect: events filtered to", events.length, "missing:", missingEventIds.size); } - - // Auto-fetch if enabled (but be conservative to avoid infinite loops) - if ($displayLimits.fetchIfNotFound && missingEventIds.size > 0 && missingEventIds.size < 20) { - debug("Auto-fetching", missingEventIds.size, "missing events"); - fetchMissingEvents(Array.from(missingEventIds)); - } } }); @@ -1061,7 +970,6 @@ onupdate={fetchEvents} onclear={clearEvents} onTagExpansionChange={handleTagExpansion} - onFetchMissing={fetchMissingEvents} {profileStats} {allEventCounts} /> diff --git a/tests/e2e/collapsible-sections.pw.spec.ts b/tests/e2e/collapsible-sections.pw.spec.ts deleted file mode 100644 index 990113f..0000000 --- a/tests/e2e/collapsible-sections.pw.spec.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Collapsible Sections UI', () => { - test.beforeEach(async ({ page }) => { - // Navigate to the visualization page - await page.goto('/visualize'); - // Wait for the visualization to load - await page.waitForSelector('.leather-legend', { timeout: 10000 }); - }); - - test.describe('Legend Component', () => { - test('should toggle main legend collapse/expand', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const legendContent = legend.locator('.legend-content'); - const toggleButton = legend.locator('button').first(); - - // Legend should be expanded by default - await expect(legendContent).toBeVisible(); - - // Click to collapse - await toggleButton.click(); - await expect(legendContent).not.toBeVisible(); - - // Click to expand - await toggleButton.click(); - await expect(legendContent).toBeVisible(); - }); - - test('should toggle Node Types section independently', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const nodeTypesSection = legend.locator('.legend-section').first(); - const nodeTypesHeader = nodeTypesSection.locator('.legend-section-header'); - const nodeTypesList = nodeTypesSection.locator('.legend-list'); - - // Node Types should be expanded by default - await expect(nodeTypesList).toBeVisible(); - - // Click header to collapse - await nodeTypesHeader.click(); - await expect(nodeTypesList).not.toBeVisible(); - - // Click header to expand - await nodeTypesHeader.click(); - await expect(nodeTypesList).toBeVisible(); - }); - - test('should toggle Tag Anchors section independently when visible', async ({ page }) => { - // First enable tag anchors in settings - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - - // Expand settings if needed - const settingsContent = settings.locator('.space-y-4'); - if (!(await settingsContent.isVisible())) { - await settingsToggle.click(); - } - - // Enable tag anchors - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - if (!(await tagAnchorsToggle.isChecked())) { - await tagAnchorsToggle.click(); - } - - // Wait for tag anchors to appear in legend - await page.waitForTimeout(1000); // Allow time for graph update - - const legend = page.locator('.leather-legend').first(); - const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - - if (await tagSection.count() > 0) { - const tagHeader = tagSection.locator('.legend-section-header'); - const tagGrid = tagSection.locator('.tag-grid'); - - // Should be expanded by default - await expect(tagGrid).toBeVisible(); - - // Click to collapse - await tagHeader.click(); - await expect(tagGrid).not.toBeVisible(); - - // Click to expand - await tagHeader.click(); - await expect(tagGrid).toBeVisible(); - } - }); - - test('should maintain section states independently', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const nodeTypesSection = legend.locator('.legend-section').first(); - const nodeTypesHeader = nodeTypesSection.locator('.legend-section-header'); - const nodeTypesList = nodeTypesSection.locator('.legend-list'); - - // Collapse Node Types section - await nodeTypesHeader.click(); - await expect(nodeTypesList).not.toBeVisible(); - - // Toggle main legend - const toggleButton = legend.locator('button').first(); - await toggleButton.click(); // Collapse - await toggleButton.click(); // Expand - - // Node Types should still be collapsed - await expect(nodeTypesList).not.toBeVisible(); - }); - }); - - test.describe('Settings Component', () => { - test('should toggle main settings collapse/expand', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const settingsContent = settings.locator('.space-y-4'); - const toggleButton = settings.locator('button').first(); - - // Settings should be collapsed by default - await expect(settingsContent).not.toBeVisible(); - - // Click to expand - await toggleButton.click(); - await expect(settingsContent).toBeVisible(); - - // Click to collapse - await toggleButton.click(); - await expect(settingsContent).not.toBeVisible(); - }); - - test('should toggle all settings sections independently', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const toggleButton = settings.locator('button').first(); - - // Expand settings - await toggleButton.click(); - - const sections = [ - { name: 'Event Types', contentSelector: 'text="Event Kind Filter"' }, - { name: 'Initial Load', contentSelector: 'text="Network Fetch Limit"' }, - { name: 'Display Limits', contentSelector: 'text="Max Publication Indices"' }, - { name: 'Graph Traversal', contentSelector: 'text="Search through already fetched"' }, - { name: 'Visual Settings', contentSelector: 'text="Star Network View"' } - ]; - - for (const section of sections) { - const sectionHeader = settings.locator('.settings-section-header').filter({ hasText: section.name }); - const sectionContent = settings.locator('.settings-section').filter({ has: sectionHeader }); - - // All sections should be expanded by default - await expect(sectionContent.locator(section.contentSelector)).toBeVisible(); - - // Click to collapse - await sectionHeader.click(); - await expect(sectionContent.locator(section.contentSelector)).not.toBeVisible(); - - // Click to expand - await sectionHeader.click(); - await expect(sectionContent.locator(section.contentSelector)).toBeVisible(); - } - }); - - test('should preserve section states when toggling main settings', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const toggleButton = settings.locator('button').first(); - - // Expand settings - await toggleButton.click(); - - // Collapse some sections - const eventTypesHeader = settings.locator('.settings-section-header').filter({ hasText: 'Event Types' }); - const displayLimitsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Display Limits' }); - - await eventTypesHeader.click(); - await displayLimitsHeader.click(); - - // Verify they are collapsed - const eventTypesContent = settings.locator('.settings-section').filter({ has: eventTypesHeader }); - const displayLimitsContent = settings.locator('.settings-section').filter({ has: displayLimitsHeader }); - - await expect(eventTypesContent.locator('text="Event Kind Filter"')).not.toBeVisible(); - await expect(displayLimitsContent.locator('text="Max Publication Indices"')).not.toBeVisible(); - - // Toggle main settings - await toggleButton.click(); // Collapse - await toggleButton.click(); // Expand - - // Sections should maintain their collapsed state - await expect(eventTypesContent.locator('text="Event Kind Filter"')).not.toBeVisible(); - await expect(displayLimitsContent.locator('text="Max Publication Indices"')).not.toBeVisible(); - - // Other sections should still be expanded - const visualSettingsContent = settings.locator('.settings-section').filter({ - has: settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }) - }); - await expect(visualSettingsContent.locator('text="Star Network View"')).toBeVisible(); - }); - - test('should show hover effect on section headers', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const toggleButton = settings.locator('button').first(); - - // Expand settings - await toggleButton.click(); - - const eventTypesHeader = settings.locator('.settings-section-header').filter({ hasText: 'Event Types' }); - - // Hover over header - await eventTypesHeader.hover(); - - // Check for hover styles (background color change) - // Note: This is a basic check, actual hover styles depend on CSS - await expect(eventTypesHeader).toHaveCSS('cursor', 'pointer'); - }); - }); - - test.describe('Icon State Changes', () => { - test('should show correct caret icons for expand/collapse states', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const settings = page.locator('.leather-legend').nth(1); - - // Check main toggle buttons - const legendToggle = legend.locator('button').first(); - const settingsToggle = settings.locator('button').first(); - - // Legend starts expanded (shows up caret) - await expect(legendToggle.locator('svg')).toHaveAttribute('class', /CaretUpOutline/); - - // Click to collapse (should show down caret) - await legendToggle.click(); - await expect(legendToggle.locator('svg')).toHaveAttribute('class', /CaretDownOutline/); - - // Settings starts collapsed (shows down caret) - await expect(settingsToggle.locator('svg')).toHaveAttribute('class', /CaretDownOutline/); - - // Click to expand (should show up caret) - await settingsToggle.click(); - await expect(settingsToggle.locator('svg')).toHaveAttribute('class', /CaretUpOutline/); - - // Check section toggles - const eventTypesHeader = settings.locator('.settings-section-header').filter({ hasText: 'Event Types' }); - const eventTypesButton = eventTypesHeader.locator('button'); - - // Section starts expanded - await expect(eventTypesButton.locator('svg')).toHaveAttribute('class', /CaretUpOutline/); - - // Click to collapse - await eventTypesHeader.click(); - await expect(eventTypesButton.locator('svg')).toHaveAttribute('class', /CaretDownOutline/); - }); - }); - - test.describe('Responsive Behavior', () => { - test('should maintain functionality on mobile viewport', async ({ page }) => { - // Set mobile viewport - await page.setViewportSize({ width: 375, height: 667 }); - - const legend = page.locator('.leather-legend').first(); - const settings = page.locator('.leather-legend').nth(1); - - // Test basic toggle functionality still works - const legendToggle = legend.locator('button').first(); - const settingsToggle = settings.locator('button').first(); - - const legendContent = legend.locator('.legend-content'); - - // Toggle legend - await expect(legendContent).toBeVisible(); - await legendToggle.click(); - await expect(legendContent).not.toBeVisible(); - - // Expand settings and test section toggle - await settingsToggle.click(); - const eventTypesHeader = settings.locator('.settings-section-header').filter({ hasText: 'Event Types' }); - const eventTypesContent = settings.locator('.settings-section').filter({ has: eventTypesHeader }); - - await expect(eventTypesContent.locator('text="Event Kind Filter"')).toBeVisible(); - await eventTypesHeader.click(); - await expect(eventTypesContent.locator('text="Event Kind Filter"')).not.toBeVisible(); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/example.pw.spec.ts b/tests/e2e/example.pw.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/tests/e2e/example.pw.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/tests/e2e/poc-performance-validation.pw.spec.ts b/tests/e2e/poc-performance-validation.pw.spec.ts deleted file mode 100644 index 29a2141..0000000 --- a/tests/e2e/poc-performance-validation.pw.spec.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// Performance thresholds based on POC targets -const PERFORMANCE_TARGETS = { - visualUpdate: 50, // <50ms for visual updates - fullUpdate: 200, // Baseline for full updates - positionDrift: 5, // Max pixels of position drift - memoryIncrease: 10 // Max % memory increase per update -}; - -test.describe('Shallow Copy POC Performance Validation', () => { - // Helper to extract console logs - const consoleLogs: string[] = []; - - test.beforeEach(async ({ page }) => { - // Clear logs - consoleLogs.length = 0; - - // Capture console logs - page.on('console', msg => { - if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - consoleLogs.push(msg.text()); - } - }); - - // Navigate to visualization page - await page.goto('http://localhost:5175/visualize'); - - // Wait for initial load - await page.waitForSelector('.network-svg', { timeout: 10000 }); - await page.waitForTimeout(2000); // Allow graph to stabilize - }); - - test('star visualization toggle uses visual update path', async ({ page }) => { - // Enable settings panel - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - // Ensure visual settings section is expanded - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - // Clear previous logs - consoleLogs.length = 0; - - // Toggle star visualization - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - await starToggle.click(); - - // Wait for update - await page.waitForTimeout(100); - - // Check logs for update type - const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - expect(updateLogs.length).toBeGreaterThan(0); - - const lastUpdateLog = updateLogs[updateLogs.length - 1]; - expect(lastUpdateLog).toContain('kind: "visual"'); - expect(lastUpdateLog).toContain('star'); - - // Check for visual properties update - const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - expect(visualUpdateLogs.length).toBeGreaterThan(0); - - // Extract timing - const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - if (timingLogs.length > 0) { - const match = timingLogs[0].match(/(\d+\.\d+)ms/); - if (match) { - const updateTime = parseFloat(match[1]); - expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - console.log(`Star toggle update time: ${updateTime}ms`); - } - } - }); - - test('tag visibility toggle uses visual update path', async ({ page }) => { - // Enable settings and tag anchors - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - // Enable tag anchors - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - await tagAnchorsToggle.click(); - - // Wait for tags to appear - await page.waitForTimeout(1000); - - const legend = page.locator('.leather-legend').first(); - const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - - if (await tagSection.count() > 0) { - // Expand tag section if needed - const tagHeader = tagSection.locator('.legend-section-header'); - const tagGrid = tagSection.locator('.tag-grid'); - if (!(await tagGrid.isVisible())) { - await tagHeader.click(); - } - - // Clear logs - consoleLogs.length = 0; - - // Toggle first tag - const firstTag = tagGrid.locator('.tag-grid-item').first(); - await firstTag.click(); - - // Wait for update - await page.waitForTimeout(100); - - // Check for visual update - const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - expect(updateLogs.length).toBeGreaterThan(0); - - const lastUpdateLog = updateLogs[updateLogs.length - 1]; - expect(lastUpdateLog).toContain('kind: "visual"'); - expect(lastUpdateLog).toContain('disabledCount'); - - // Check timing - const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - if (timingLogs.length > 0) { - const match = timingLogs[0].match(/(\d+\.\d+)ms/); - if (match) { - const updateTime = parseFloat(match[1]); - expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - console.log(`Tag toggle update time: ${updateTime}ms`); - } - } - } - }); - - test('position preservation during visual updates', async ({ page }) => { - // Get initial node positions - const getNodePositions = async () => { - return await page.evaluate(() => { - const nodes = document.querySelectorAll('.network-svg g.node'); - const positions: { [id: string]: { x: number; y: number } } = {}; - - nodes.forEach((node) => { - const transform = node.getAttribute('transform'); - const match = transform?.match(/translate\(([\d.-]+),([\d.-]+)\)/); - if (match) { - const nodeId = (node as any).__data__?.id || 'unknown'; - positions[nodeId] = { - x: parseFloat(match[1]), - y: parseFloat(match[2]) - }; - } - }); - - return positions; - }); - }; - - // Capture initial positions - const initialPositions = await getNodePositions(); - const nodeCount = Object.keys(initialPositions).length; - expect(nodeCount).toBeGreaterThan(0); - - // Toggle star visualization - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - await starToggle.click(); - - // Wait for visual update - await page.waitForTimeout(500); - - // Get positions after update - const updatedPositions = await getNodePositions(); - - // Check position preservation - let maxDrift = 0; - let driftCount = 0; - - Object.keys(initialPositions).forEach(nodeId => { - if (updatedPositions[nodeId]) { - const initial = initialPositions[nodeId]; - const updated = updatedPositions[nodeId]; - const drift = Math.sqrt( - Math.pow(updated.x - initial.x, 2) + - Math.pow(updated.y - initial.y, 2) - ); - - if (drift > PERFORMANCE_TARGETS.positionDrift) { - driftCount++; - maxDrift = Math.max(maxDrift, drift); - } - } - }); - - // Positions should be mostly preserved (some drift due to force changes is OK) - const driftPercentage = (driftCount / nodeCount) * 100; - expect(driftPercentage).toBeLessThan(20); // Less than 20% of nodes should drift significantly - console.log(`Position drift: ${driftCount}/${nodeCount} nodes (${driftPercentage.toFixed(1)}%), max drift: ${maxDrift.toFixed(1)}px`); - }); - - test('simulation maintains momentum', async ({ page }) => { - // Check simulation alpha values in logs - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - // Clear logs - consoleLogs.length = 0; - - // Toggle star mode - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - await starToggle.click(); - - await page.waitForTimeout(100); - - // Check for gentle restart - const alphaLogs = consoleLogs.filter(log => log.includes('simulation restarted with alpha')); - expect(alphaLogs.length).toBeGreaterThan(0); - - // Should use alpha 0.3 for visual updates - expect(alphaLogs[0]).toContain('alpha 0.3'); - }); - - test('rapid parameter changes are handled efficiently', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - // Clear logs - consoleLogs.length = 0; - - // Perform rapid toggles - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - - const startTime = Date.now(); - for (let i = 0; i < 5; i++) { - await starToggle.click(); - await page.waitForTimeout(50); // Very short delay - } - const totalTime = Date.now() - startTime; - - // Check that all updates completed - await page.waitForTimeout(500); - - // Count visual updates - const visualUpdateCount = consoleLogs.filter(log => log.includes('updateVisualProperties called')).length; - expect(visualUpdateCount).toBeGreaterThanOrEqual(3); // At least some updates should process - - console.log(`Rapid toggle test: ${visualUpdateCount} visual updates in ${totalTime}ms`); - }); - - test('memory stability during visual updates', async ({ page }) => { - // Get initial memory usage - const getMemoryUsage = async () => { - return await page.evaluate(() => { - if ('memory' in performance) { - return (performance as any).memory.usedJSHeapSize; - } - return 0; - }); - }; - - const initialMemory = await getMemoryUsage(); - if (initialMemory === 0) { - test.skip(); - return; - } - - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - - // Perform multiple toggles - for (let i = 0; i < 10; i++) { - await starToggle.click(); - await page.waitForTimeout(100); - } - - // Force garbage collection if available - await page.evaluate(() => { - if ('gc' in window) { - (window as any).gc(); - } - }); - - await page.waitForTimeout(1000); - - const finalMemory = await getMemoryUsage(); - const memoryIncrease = ((finalMemory - initialMemory) / initialMemory) * 100; - - console.log(`Memory usage: Initial ${(initialMemory / 1024 / 1024).toFixed(2)}MB, Final ${(finalMemory / 1024 / 1024).toFixed(2)}MB, Increase: ${memoryIncrease.toFixed(2)}%`); - - // Memory increase should be minimal - expect(memoryIncrease).toBeLessThan(PERFORMANCE_TARGETS.memoryIncrease); - }); - - test('comparison: visual update vs full update performance', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - await settingsToggle.click(); - - // Test visual update (star toggle) - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - await visualSettingsHeader.click(); - - consoleLogs.length = 0; - const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - await starToggle.click(); - await page.waitForTimeout(200); - - let visualUpdateTime = 0; - const visualTimingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - if (visualTimingLogs.length > 0) { - const match = visualTimingLogs[0].match(/(\d+\.\d+)ms/); - if (match) { - visualUpdateTime = parseFloat(match[1]); - } - } - - // Test full update (fetch limit change) - const initialLoadHeader = settings.locator('.settings-section-header').filter({ hasText: 'Initial Load' }); - await initialLoadHeader.click(); - - consoleLogs.length = 0; - const fetchLimitInput = settings.locator('input[type="number"]').first(); - await fetchLimitInput.fill('20'); - await page.keyboard.press('Enter'); - await page.waitForTimeout(500); - - let fullUpdateTime = 0; - const fullTimingLogs = consoleLogs.filter(log => log.includes('updateGraph completed in')); - if (fullTimingLogs.length > 0) { - const match = fullTimingLogs[0].match(/(\d+\.\d+)ms/); - if (match) { - fullUpdateTime = parseFloat(match[1]); - } - } - - console.log(`Performance comparison: - - Visual update: ${visualUpdateTime.toFixed(2)}ms - - Full update: ${fullUpdateTime.toFixed(2)}ms - - Improvement: ${((1 - visualUpdateTime / fullUpdateTime) * 100).toFixed(1)}%`); - - // Visual updates should be significantly faster - expect(visualUpdateTime).toBeLessThan(fullUpdateTime * 0.5); // At least 50% faster - expect(visualUpdateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - }); -}); \ No newline at end of file diff --git a/tests/e2e/tag-anchor-interactions.pw.spec.ts b/tests/e2e/tag-anchor-interactions.pw.spec.ts deleted file mode 100644 index 3291020..0000000 --- a/tests/e2e/tag-anchor-interactions.pw.spec.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test.describe('Tag Anchor Interactive Features', () => { - test.beforeEach(async ({ page }) => { - // Navigate to visualization page - await page.goto('/visualize'); - - // Wait for visualization to load - await page.waitForSelector('.leather-legend', { timeout: 10000 }); - - // Enable tag anchors in settings - const settings = page.locator('.leather-legend').nth(1); - const settingsToggle = settings.locator('button').first(); - - // Expand settings if needed - const settingsContent = settings.locator('.space-y-4'); - if (!(await settingsContent.isVisible())) { - await settingsToggle.click(); - } - - // Expand Visual Settings section - const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - const visualSettingsContent = settings.locator('.settings-section').filter({ has: visualSettingsHeader }); - - // Check if section is collapsed and expand if needed - const starNetworkToggle = visualSettingsContent.locator('text="Star Network View"'); - if (!(await starNetworkToggle.isVisible())) { - await visualSettingsHeader.click(); - } - - // Enable tag anchors - const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - if (!(await tagAnchorsToggle.isChecked())) { - await tagAnchorsToggle.click(); - } - - // Wait for graph to update - await page.waitForTimeout(1000); - }); - - test('should display tag anchors in legend when enabled', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - - // Check for tag anchors section - const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - await expect(tagSection).toBeVisible(); - - // Verify tag grid is displayed - const tagGrid = tagSection.locator('.tag-grid'); - await expect(tagGrid).toBeVisible(); - - // Should have tag items - const tagItems = tagGrid.locator('.tag-grid-item'); - const count = await tagItems.count(); - expect(count).toBeGreaterThan(0); - }); - - test('should toggle individual tag anchors on click', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Get first tag anchor - const firstTag = tagGrid.locator('.tag-grid-item').first(); - const tagLabel = await firstTag.locator('.legend-text').textContent(); - - // Click to disable - await firstTag.click(); - - // Should have disabled class - await expect(firstTag).toHaveClass(/disabled/); - - // Visual indicators should show disabled state - const tagCircle = firstTag.locator('.legend-circle'); - await expect(tagCircle).toHaveCSS('opacity', '0.3'); - - const tagText = firstTag.locator('.legend-text'); - await expect(tagText).toHaveCSS('opacity', '0.5'); - - // Click again to enable - await firstTag.click(); - - // Should not have disabled class - await expect(firstTag).not.toHaveClass(/disabled/); - - // Visual indicators should show enabled state - await expect(tagCircle).toHaveCSS('opacity', '1'); - await expect(tagText).toHaveCSS('opacity', '1'); - }); - - test('should show correct tooltip on hover', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Get first tag anchor - const firstTag = tagGrid.locator('.tag-grid-item').first(); - - // Hover over tag - await firstTag.hover(); - - // Check title attribute - const title = await firstTag.getAttribute('title'); - expect(title).toContain('Click to'); - - // Disable the tag - await firstTag.click(); - await firstTag.hover(); - - // Title should update - const updatedTitle = await firstTag.getAttribute('title'); - expect(updatedTitle).toContain('Click to enable'); - }); - - test('should maintain disabled state across legend collapse', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Disable some tags - const firstTag = tagGrid.locator('.tag-grid-item').first(); - const secondTag = tagGrid.locator('.tag-grid-item').nth(1); - - await firstTag.click(); - await secondTag.click(); - - // Verify disabled - await expect(firstTag).toHaveClass(/disabled/); - await expect(secondTag).toHaveClass(/disabled/); - - // Collapse and expand tag section - const tagSectionHeader = legend.locator('.legend-section-header').filter({ hasText: 'Active Tag Anchors' }); - await tagSectionHeader.click(); // Collapse - await tagSectionHeader.click(); // Expand - - // Tags should still be disabled - await expect(firstTag).toHaveClass(/disabled/); - await expect(secondTag).toHaveClass(/disabled/); - }); - - test('should handle tag type changes correctly', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const legend = page.locator('.leather-legend').first(); - - // Change tag type - const tagTypeSelect = settings.locator('#tag-type-select'); - await tagTypeSelect.selectOption('p'); // Change to People (Pubkeys) - - // Wait for update - await page.waitForTimeout(500); - - // Check legend updates - const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - const sectionTitle = tagSection.locator('.legend-section-title'); - - await expect(sectionTitle).toContainText('Active Tag Anchors: p'); - - // Tag grid should update with new tags - const tagItems = tagSection.locator('.tag-grid-item'); - const firstTagIcon = tagItems.first().locator('.legend-letter'); - - // Should show 'A' for author type - await expect(firstTagIcon).toContainText('A'); - }); - - test('should show correct tag type icons', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const legend = page.locator('.leather-legend').first(); - - const tagTypes = [ - { value: 't', icon: '#' }, - { value: 'author', icon: 'A' }, - { value: 'p', icon: 'P' }, - { value: 'e', icon: 'E' }, - { value: 'title', icon: 'T' }, - { value: 'summary', icon: 'S' } - ]; - - for (const { value, icon } of tagTypes) { - // Change tag type - const tagTypeSelect = settings.locator('#tag-type-select'); - await tagTypeSelect.selectOption(value); - - // Wait for update - await page.waitForTimeout(500); - - // Check icon - const tagGrid = legend.locator('.tag-grid'); - const tagItems = tagGrid.locator('.tag-grid-item'); - - if (await tagItems.count() > 0) { - const firstTagIcon = tagItems.first().locator('.legend-letter'); - await expect(firstTagIcon).toContainText(icon); - } - } - }); - - test('should handle empty tag lists gracefully', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const legend = page.locator('.leather-legend').first(); - - // Try different tag types that might have no results - const tagTypeSelect = settings.locator('#tag-type-select'); - await tagTypeSelect.selectOption('summary'); - - // Wait for update - await page.waitForTimeout(500); - - // Check if tag section exists - const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - const tagSectionCount = await tagSection.count(); - - if (tagSectionCount === 0) { - // No tag section should be shown if no tags - expect(tagSectionCount).toBe(0); - } else { - // If section exists, check for empty state - const tagGrid = tagSection.locator('.tag-grid'); - const tagItems = tagGrid.locator('.tag-grid-item'); - const itemCount = await tagItems.count(); - - // Should handle empty state gracefully - expect(itemCount).toBeGreaterThanOrEqual(0); - } - }); - - test('should update graph when tags are toggled', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Get initial graph state (count visible nodes) - const graphContainer = page.locator('svg.network-graph'); - const initialNodes = await graphContainer.locator('circle').count(); - - // Disable a tag - const firstTag = tagGrid.locator('.tag-grid-item').first(); - await firstTag.click(); - - // Wait for graph update - await page.waitForTimeout(500); - - // Graph should update (implementation specific - might hide nodes or change styling) - // This is a placeholder assertion - actual behavior depends on implementation - const updatedNodes = await graphContainer.locator('circle').count(); - - // Nodes might be hidden or styled differently - // The exact assertion depends on how disabled tags affect the visualization - expect(updatedNodes).toBeGreaterThanOrEqual(0); - }); - - test('should work with keyboard navigation', async ({ page }) => { - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Focus first tag - const firstTag = tagGrid.locator('.tag-grid-item').first(); - await firstTag.focus(); - - // Press Enter to toggle - await page.keyboard.press('Enter'); - - // Should be disabled - await expect(firstTag).toHaveClass(/disabled/); - - // Press Enter again - await page.keyboard.press('Enter'); - - // Should be enabled - await expect(firstTag).not.toHaveClass(/disabled/); - - // Tab to next tag - await page.keyboard.press('Tab'); - - // Should focus next tag - const secondTag = tagGrid.locator('.tag-grid-item').nth(1); - await expect(secondTag).toBeFocused(); - }); - - test('should persist state through tag type changes', async ({ page }) => { - const settings = page.locator('.leather-legend').nth(1); - const legend = page.locator('.leather-legend').first(); - const tagGrid = legend.locator('.tag-grid'); - - // Disable some hashtags - const firstHashtag = tagGrid.locator('.tag-grid-item').first(); - await firstHashtag.click(); - - // Change to authors - const tagTypeSelect = settings.locator('#tag-type-select'); - await tagTypeSelect.selectOption('author'); - await page.waitForTimeout(500); - - // Disable an author tag - const firstAuthor = tagGrid.locator('.tag-grid-item').first(); - await firstAuthor.click(); - - // Switch back to hashtags - await tagTypeSelect.selectOption('t'); - await page.waitForTimeout(500); - - // Original hashtag should still be disabled - // Note: This assumes state persistence per tag type - const hashtagsAgain = tagGrid.locator('.tag-grid-item'); - if (await hashtagsAgain.count() > 0) { - // Implementation specific - check if state is preserved - const firstHashtagAgain = hashtagsAgain.first(); - // State might or might not be preserved depending on implementation - await expect(firstHashtagAgain).toBeVisible(); - } - }); -}); \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md deleted file mode 100644 index bd30e2a..0000000 --- a/tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> position preservation during visual updates -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:136:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md deleted file mode 100644 index 1982ea0..0000000 --- a/tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> rapid parameter changes are handled efficiently -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:233:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md deleted file mode 100644 index 54e29df..0000000 --- a/tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> simulation maintains momentum -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:207:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md deleted file mode 100644 index cc6dc27..0000000 --- a/tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> tag visibility toggle uses visual update path -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:78:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md deleted file mode 100644 index 604bfbc..0000000 --- a/tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> comparison: visual update vs full update performance -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:314:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md deleted file mode 100644 index 74c0c2e..0000000 --- a/tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> star visualization toggle uses visual update path -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:34:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md b/tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md deleted file mode 100644 index e55a9a4..0000000 --- a/tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Test info - -- Name: Shallow Copy POC Performance Validation >> memory stability during visual updates -- Location: /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:264:3 - -# Error details - -``` -TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. -Call log: - - waiting for locator('.network-svg') to be visible - - at /home/user/Documents/Programming/gc-alexandria/tests/e2e/poc-performance-validation.pw.spec.ts:30:16 -``` - -# Test source - -```ts - 1 | import { test, expect } from '@playwright/test'; - 2 | - 3 | // Performance thresholds based on POC targets - 4 | const PERFORMANCE_TARGETS = { - 5 | visualUpdate: 50, // <50ms for visual updates - 6 | fullUpdate: 200, // Baseline for full updates - 7 | positionDrift: 5, // Max pixels of position drift - 8 | memoryIncrease: 10 // Max % memory increase per update - 9 | }; - 10 | - 11 | test.describe('Shallow Copy POC Performance Validation', () => { - 12 | // Helper to extract console logs - 13 | const consoleLogs: string[] = []; - 14 | - 15 | test.beforeEach(async ({ page }) => { - 16 | // Clear logs - 17 | consoleLogs.length = 0; - 18 | - 19 | // Capture console logs - 20 | page.on('console', msg => { - 21 | if (msg.type() === 'log' && msg.text().includes('[EventNetwork]')) { - 22 | consoleLogs.push(msg.text()); - 23 | } - 24 | }); - 25 | - 26 | // Navigate to visualization page - 27 | await page.goto('http://localhost:5175/visualize'); - 28 | - 29 | // Wait for initial load -> 30 | await page.waitForSelector('.network-svg', { timeout: 10000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. - 31 | await page.waitForTimeout(2000); // Allow graph to stabilize - 32 | }); - 33 | - 34 | test('star visualization toggle uses visual update path', async ({ page }) => { - 35 | // Enable settings panel - 36 | const settings = page.locator('.leather-legend').nth(1); - 37 | const settingsToggle = settings.locator('button').first(); - 38 | await settingsToggle.click(); - 39 | - 40 | // Ensure visual settings section is expanded - 41 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 42 | await visualSettingsHeader.click(); - 43 | - 44 | // Clear previous logs - 45 | consoleLogs.length = 0; - 46 | - 47 | // Toggle star visualization - 48 | const starToggle = settings.locator('label').filter({ hasText: 'Star Network View' }).locator('input[type="checkbox"]'); - 49 | await starToggle.click(); - 50 | - 51 | // Wait for update - 52 | await page.waitForTimeout(100); - 53 | - 54 | // Check logs for update type - 55 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 56 | expect(updateLogs.length).toBeGreaterThan(0); - 57 | - 58 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 59 | expect(lastUpdateLog).toContain('kind: "visual"'); - 60 | expect(lastUpdateLog).toContain('star'); - 61 | - 62 | // Check for visual properties update - 63 | const visualUpdateLogs = consoleLogs.filter(log => log.includes('updateVisualProperties called')); - 64 | expect(visualUpdateLogs.length).toBeGreaterThan(0); - 65 | - 66 | // Extract timing - 67 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 68 | if (timingLogs.length > 0) { - 69 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 70 | if (match) { - 71 | const updateTime = parseFloat(match[1]); - 72 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 73 | console.log(`Star toggle update time: ${updateTime}ms`); - 74 | } - 75 | } - 76 | }); - 77 | - 78 | test('tag visibility toggle uses visual update path', async ({ page }) => { - 79 | // Enable settings and tag anchors - 80 | const settings = page.locator('.leather-legend').nth(1); - 81 | const settingsToggle = settings.locator('button').first(); - 82 | await settingsToggle.click(); - 83 | - 84 | // Enable tag anchors - 85 | const visualSettingsHeader = settings.locator('.settings-section-header').filter({ hasText: 'Visual Settings' }); - 86 | await visualSettingsHeader.click(); - 87 | - 88 | const tagAnchorsToggle = settings.locator('label').filter({ hasText: 'Show Tag Anchors' }).locator('input[type="checkbox"]'); - 89 | await tagAnchorsToggle.click(); - 90 | - 91 | // Wait for tags to appear - 92 | await page.waitForTimeout(1000); - 93 | - 94 | const legend = page.locator('.leather-legend').first(); - 95 | const tagSection = legend.locator('.legend-section').filter({ hasText: 'Active Tag Anchors' }); - 96 | - 97 | if (await tagSection.count() > 0) { - 98 | // Expand tag section if needed - 99 | const tagHeader = tagSection.locator('.legend-section-header'); - 100 | const tagGrid = tagSection.locator('.tag-grid'); - 101 | if (!(await tagGrid.isVisible())) { - 102 | await tagHeader.click(); - 103 | } - 104 | - 105 | // Clear logs - 106 | consoleLogs.length = 0; - 107 | - 108 | // Toggle first tag - 109 | const firstTag = tagGrid.locator('.tag-grid-item').first(); - 110 | await firstTag.click(); - 111 | - 112 | // Wait for update - 113 | await page.waitForTimeout(100); - 114 | - 115 | // Check for visual update - 116 | const updateLogs = consoleLogs.filter(log => log.includes('Update type detected')); - 117 | expect(updateLogs.length).toBeGreaterThan(0); - 118 | - 119 | const lastUpdateLog = updateLogs[updateLogs.length - 1]; - 120 | expect(lastUpdateLog).toContain('kind: "visual"'); - 121 | expect(lastUpdateLog).toContain('disabledCount'); - 122 | - 123 | // Check timing - 124 | const timingLogs = consoleLogs.filter(log => log.includes('Visual properties updated in')); - 125 | if (timingLogs.length > 0) { - 126 | const match = timingLogs[0].match(/(\d+\.\d+)ms/); - 127 | if (match) { - 128 | const updateTime = parseFloat(match[1]); - 129 | expect(updateTime).toBeLessThan(PERFORMANCE_TARGETS.visualUpdate); - 130 | console.log(`Tag toggle update time: ${updateTime}ms`); -``` \ No newline at end of file diff --git a/tests/integration/displayLimitsIntegration.test.ts b/tests/integration/displayLimitsIntegration.test.ts deleted file mode 100644 index c3875a7..0000000 --- a/tests/integration/displayLimitsIntegration.test.ts +++ /dev/null @@ -1,382 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { writable, get } from 'svelte/store'; -import { displayLimits } from '$lib/stores/displayLimits'; -import { visualizationConfig } from '$lib/stores/visualizationConfig'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; - -// Mock NDK Event for testing -function createMockEvent(kind: number, id: string): NDKEvent { - return { - id, - kind, - pubkey: 'mock-pubkey', - created_at: Date.now() / 1000, - content: `Mock content for ${id}`, - tags: [] - } as NDKEvent; -} - -describe('Display Limits Integration', () => { - beforeEach(() => { - // Reset stores to default values - displayLimits.set({ - max30040: -1, - max30041: -1, - fetchIfNotFound: false - }); - - visualizationConfig.setMaxPublicationIndices(-1); - visualizationConfig.setMaxEventsPerIndex(-1); - }); - - describe('Event Filtering with Limits', () => { - it('should filter events when limits are set', () => { - const events = [ - createMockEvent(30040, 'index1'), - createMockEvent(30040, 'index2'), - createMockEvent(30040, 'index3'), - createMockEvent(30041, 'content1'), - createMockEvent(30041, 'content2'), - createMockEvent(30041, 'content3'), - createMockEvent(30041, 'content4') - ]; - - // Apply display limits - const limits = get(displayLimits); - limits.max30040 = 2; - limits.max30041 = 3; - - // Filter function - const filterByLimits = (events: NDKEvent[], limits: any) => { - const kindCounts = new Map(); - - return events.filter(event => { - const count = kindCounts.get(event.kind) || 0; - - if (event.kind === 30040 && limits.max30040 !== -1 && count >= limits.max30040) { - return false; - } - if (event.kind === 30041 && limits.max30041 !== -1 && count >= limits.max30041) { - return false; - } - - kindCounts.set(event.kind, count + 1); - return true; - }); - }; - - const filtered = filterByLimits(events, limits); - - // Should have 2 index events and 3 content events - expect(filtered.filter(e => e.kind === 30040)).toHaveLength(2); - expect(filtered.filter(e => e.kind === 30041)).toHaveLength(3); - expect(filtered).toHaveLength(5); - }); - - it('should respect unlimited (-1) values', () => { - const events = Array.from({ length: 100 }, (_, i) => - createMockEvent(i % 2 === 0 ? 30040 : 30041, `event${i}`) - ); - - // Set one limit, leave other unlimited - displayLimits.update(limits => ({ - ...limits, - max30040: 10, - max30041: -1 - })); - - const limits = get(displayLimits); - const filtered = events.filter((event, index) => { - if (event.kind === 30040) { - const count = events.slice(0, index).filter(e => e.kind === 30040).length; - return limits.max30040 === -1 || count < limits.max30040; - } - return true; // No limit on 30041 - }); - - // Should have exactly 10 kind 30040 events - expect(filtered.filter(e => e.kind === 30040)).toHaveLength(10); - // Should have all 50 kind 30041 events - expect(filtered.filter(e => e.kind === 30041)).toHaveLength(50); - }); - }); - - describe('Publication Index Limits', () => { - it('should limit publication indices separately from content', () => { - const config = get(visualizationConfig); - - // Create publication structure - const publications = [ - { - index: createMockEvent(30040, 'pub1'), - content: [ - createMockEvent(30041, 'pub1-content1'), - createMockEvent(30041, 'pub1-content2'), - createMockEvent(30041, 'pub1-content3') - ] - }, - { - index: createMockEvent(30040, 'pub2'), - content: [ - createMockEvent(30041, 'pub2-content1'), - createMockEvent(30041, 'pub2-content2') - ] - }, - { - index: createMockEvent(30040, 'pub3'), - content: [ - createMockEvent(30041, 'pub3-content1') - ] - } - ]; - - // Set limits - visualizationConfig.setMaxPublicationIndices(2); - visualizationConfig.setMaxEventsPerIndex(2); - - // Apply limits - const limitedPubs = publications - .slice(0, get(visualizationConfig).maxPublicationIndices === -1 - ? publications.length - : get(visualizationConfig).maxPublicationIndices) - .map(pub => ({ - index: pub.index, - content: pub.content.slice(0, get(visualizationConfig).maxEventsPerIndex === -1 - ? pub.content.length - : get(visualizationConfig).maxEventsPerIndex) - })); - - // Should have 2 publications - expect(limitedPubs).toHaveLength(2); - // First pub should have 2 content events - expect(limitedPubs[0].content).toHaveLength(2); - // Second pub should have 2 content events - expect(limitedPubs[1].content).toHaveLength(2); - }); - - it('should handle per-index limits correctly', () => { - visualizationConfig.setMaxEventsPerIndex(3); - const maxPerIndex = get(visualizationConfig).maxEventsPerIndex; - - const indexEvents = new Map(); - - // Simulate grouping events by index - const events = [ - { indexId: 'idx1', event: createMockEvent(30041, 'c1') }, - { indexId: 'idx1', event: createMockEvent(30041, 'c2') }, - { indexId: 'idx1', event: createMockEvent(30041, 'c3') }, - { indexId: 'idx1', event: createMockEvent(30041, 'c4') }, // Should be filtered - { indexId: 'idx2', event: createMockEvent(30041, 'c5') }, - { indexId: 'idx2', event: createMockEvent(30041, 'c6') } - ]; - - events.forEach(({ indexId, event }) => { - const current = indexEvents.get(indexId) || []; - if (maxPerIndex === -1 || current.length < maxPerIndex) { - indexEvents.set(indexId, [...current, event]); - } - }); - - // idx1 should have 3 events - expect(indexEvents.get('idx1')).toHaveLength(3); - // idx2 should have 2 events - expect(indexEvents.get('idx2')).toHaveLength(2); - }); - }); - - describe('Fetch If Not Found Feature', () => { - it('should identify missing referenced events', () => { - const availableEvents = new Set(['event1', 'event2', 'event3']); - const referencedEvents = ['event1', 'event2', 'event4', 'event5']; - - displayLimits.update(limits => ({ - ...limits, - fetchIfNotFound: true - })); - - const limits = get(displayLimits); - const missingEvents = limits.fetchIfNotFound - ? referencedEvents.filter(id => !availableEvents.has(id)) - : []; - - expect(missingEvents).toEqual(['event4', 'event5']); - }); - - it('should not fetch when fetchIfNotFound is false', () => { - const availableEvents = new Set(['event1']); - const referencedEvents = ['event1', 'event2', 'event3']; - - displayLimits.update(limits => ({ - ...limits, - fetchIfNotFound: false - })); - - const limits = get(displayLimits); - const shouldFetch = limits.fetchIfNotFound && - referencedEvents.some(id => !availableEvents.has(id)); - - expect(shouldFetch).toBe(false); - }); - - it('should batch fetch requests for missing events', () => { - const fetchQueue: string[] = []; - const addToFetchQueue = (ids: string[]) => { - fetchQueue.push(...ids); - }; - - // Simulate finding missing events - const missingEvents = ['event10', 'event11', 'event12']; - - displayLimits.update(limits => ({ - ...limits, - fetchIfNotFound: true - })); - - if (get(displayLimits).fetchIfNotFound) { - addToFetchQueue(missingEvents); - } - - expect(fetchQueue).toEqual(missingEvents); - expect(fetchQueue).toHaveLength(3); - }); - }); - - describe('Integration with Visualization Updates', () => { - it('should trigger appropriate updates when limits change', () => { - const updateTypes: string[] = []; - const mockUpdate = (type: string) => updateTypes.push(type); - - // Change publication index limit - const oldConfig = get(visualizationConfig); - visualizationConfig.setMaxPublicationIndices(5); - - if (get(visualizationConfig).maxPublicationIndices !== oldConfig.maxPublicationIndices) { - mockUpdate('filter-indices'); - } - - // Change events per index limit - visualizationConfig.setMaxEventsPerIndex(10); - mockUpdate('filter-content'); - - // Toggle fetchIfNotFound - displayLimits.update(limits => ({ - ...limits, - fetchIfNotFound: true - })); - mockUpdate('check-missing'); - - expect(updateTypes).toContain('filter-indices'); - expect(updateTypes).toContain('filter-content'); - expect(updateTypes).toContain('check-missing'); - }); - - it('should preserve existing graph structure when applying limits', () => { - const graph = { - nodes: [ - { id: 'idx1', type: 'index' }, - { id: 'c1', type: 'content' }, - { id: 'c2', type: 'content' }, - { id: 'c3', type: 'content' } - ], - links: [ - { source: 'idx1', target: 'c1' }, - { source: 'idx1', target: 'c2' }, - { source: 'idx1', target: 'c3' } - ] - }; - - // Apply content limit - visualizationConfig.setMaxEventsPerIndex(2); - const limit = get(visualizationConfig).maxEventsPerIndex; - - // Filter nodes and links based on limit - const contentNodes = graph.nodes.filter(n => n.type === 'content'); - const limitedContentIds = contentNodes.slice(0, limit).map(n => n.id); - - const filteredGraph = { - nodes: graph.nodes.filter(n => - n.type !== 'content' || limitedContentIds.includes(n.id) - ), - links: graph.links.filter(l => - limitedContentIds.includes(l.target) - ) - }; - - expect(filteredGraph.nodes).toHaveLength(3); // 1 index + 2 content - expect(filteredGraph.links).toHaveLength(2); - }); - }); - - describe('Performance Considerations', () => { - it('should handle large event sets efficiently', () => { - const largeEventSet = Array.from({ length: 10000 }, (_, i) => - createMockEvent(i % 2 === 0 ? 30040 : 30041, `event${i}`) - ); - - const startTime = performance.now(); - - // Apply strict limits - displayLimits.update(limits => ({ - ...limits, - max30040: 50, - max30041: 100 - })); - - const limits = get(displayLimits); - const kindCounts = new Map(); - - const filtered = largeEventSet.filter(event => { - const count = kindCounts.get(event.kind) || 0; - - if (event.kind === 30040 && limits.max30040 !== -1 && count >= limits.max30040) { - return false; - } - if (event.kind === 30041 && limits.max30041 !== -1 && count >= limits.max30041) { - return false; - } - - kindCounts.set(event.kind, count + 1); - return true; - }); - - const endTime = performance.now(); - const filterTime = endTime - startTime; - - // Should complete quickly even with large sets - expect(filterTime).toBeLessThan(100); // 100ms threshold - expect(filtered).toHaveLength(150); // 50 + 100 - }); - - it('should cache limit calculations when possible', () => { - let calculationCount = 0; - - const getCachedLimits = (() => { - let cache: any = null; - let cacheKey: string = ''; - - return (limits: any) => { - const key = JSON.stringify(limits); - if (key !== cacheKey) { - calculationCount++; - cache = { ...limits, calculated: true }; - cacheKey = key; - } - return cache; - }; - })(); - - // First call - should calculate - getCachedLimits(get(displayLimits)); - expect(calculationCount).toBe(1); - - // Same limits - should use cache - getCachedLimits(get(displayLimits)); - expect(calculationCount).toBe(1); - - // Change limits - should recalculate - displayLimits.update(limits => ({ ...limits, max30040: 10 })); - getCachedLimits(get(displayLimits)); - expect(calculationCount).toBe(2); - }); - }); -}); \ No newline at end of file diff --git a/tests/integration/markupIntegration.test.ts b/tests/integration/markupIntegration.test.ts deleted file mode 100644 index b4de512..0000000 --- a/tests/integration/markupIntegration.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseBasicmarkup } from '../../src/lib/utils/markup/basicMarkupParser'; -import { parseAdvancedmarkup } from '../../src/lib/utils/markup/advancedMarkupParser'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -const testFilePath = join(__dirname, './markupTestfile.md'); -const md = readFileSync(testFilePath, 'utf-8'); - -describe('Markup Integration Test', () => { - it('parses markupTestfile.md with the basic parser', async () => { - const output = await parseBasicmarkup(md); - // Headers (should be present as text, not

tags) - expect(output).toContain('This is a test'); - expect(output).toContain('============'); - expect(output).toContain('### Disclaimer'); - // Unordered list - expect(output).toContain(']*>.*]*>/s); - // Blockquotes - expect(output).toContain(''); - // Images - expect(output).toMatch(/]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/); - // Links - expect(output).toMatch(/]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); - // Hashtags - expect(output).toContain('text-primary-600'); - // Nostr identifiers (should be Alexandria links) - expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); - // Wikilinks - expect(output).toContain('wikilink'); - // YouTube iframe - expect(output).toMatch(/]+youtube/); - // Tracking token removal: should not contain utm_, fbclid, or gclid in any link - expect(output).not.toMatch(/utm_/); - expect(output).not.toMatch(/fbclid/); - expect(output).not.toMatch(/gclid/); - // Horizontal rule (should be present as --- in basic) - expect(output).toContain('---'); - // Footnote references (should be present as [^1] in basic) - expect(output).toContain('[^1]'); - // Table (should be present as | Syntax | Description | in basic) - expect(output).toContain('| Syntax | Description |'); - }); - - it('parses markupTestfile.md with the advanced parser', async () => { - const output = await parseAdvancedmarkup(md); - // Headers - expect(output).toContain(']*>.*]*>/s); - // Blockquotes - expect(output).toContain(']*>.*leather min-h-full w-full flex flex-col items-center.*<\/code>/s); - // Images - expect(output).toMatch(/]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/); - // Links - expect(output).toMatch(/]+href="https:\/\/github.com\/nostrability\/nostrability\/issues\/146"/); - // Hashtags - expect(output).toContain('text-primary-600'); - // Nostr identifiers (should be Alexandria links) - expect(output).toContain('./events?id=npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z'); - // Wikilinks - expect(output).toContain('wikilink'); - // YouTube iframe - expect(output).toMatch(/]+youtube/); - // Tracking token removal: should not contain utm_, fbclid, or gclid in any link - expect(output).not.toMatch(/utm_/); - expect(output).not.toMatch(/fbclid/); - expect(output).not.toMatch(/gclid/); - // Horizontal rule - expect(output).toContain('/); - // Table - expect(output).toContain(' This is important information - -> This is multiple -> lines of -> important information -> with a second[^2] footnote. -[^2]: This is a "Test" of a longer footnote-reference, placed inline, including some punctuation. 1984. - -This is a youtube link -https://www.youtube.com/watch?v=9aqVxNCpx9s - -And here is a link with tracking tokens: -https://arstechnica.com/science/2019/07/new-data-may-extend-norse-occupancy-in-north-america/?fbclid=IwAR1LOW3BebaMLinfkWFtFpzkLFi48jKNF7P6DV2Ux2r3lnT6Lqj6eiiOZNU - -This is an unordered list: -* but -* not -* really - -This is an unordered list with nesting: -* but - * not - * really -* but - * yes, - * really - -## More testing - -An ordered list: -1. first -2. second -3. third - -Let's nest that: -1. first - 2. second indented -3. third - 4. fourth indented - 5. fifth indented even more - 6. sixth under the fourth - 7. seventh under the sixth -8. eighth under the third - -This is ordered and unordered mixed: -1. first - 2. second indented -3. third - * make this a bullet point - 4. fourth indented even more - * second bullet point - -Here is a horizontal rule: - ---- - -Try embedded a nostr note with nevent: - -nostr:nevent1qvzqqqqqqypzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqyrzdyycehfwyekef75z5wnnygqeps6a4qvc8dunvumzr08g06svgcptkske - -Here a note with no prefix - -note1cnfpxxd6t3xdk204q4r5uezqxgvxhdgrxpm0ym8xcsme6r75rzxqcj9lmz - -Here with a naddr: - -nostr:naddr1qvzqqqr4gupzplfq3m5v3u5r0q9f255fdeyz8nyac6lagssx8zy4wugxjs8ajf7pqydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqzasj6ar9wd6xv6tvv5kkvmmj94kkzuntv3hhwmsu0ktnz - -Here's a nonsense one: - -nevent123 - -And a nonsense one with a prefix: - -nostr:naddrwhatever - -And some Nostr addresses that should be preserved and have a internal link appended: - -https://lumina.rocks/note/note1sd0hkhxr49jsetkcrjkvf2uls5m8frkue6f5huj8uv4964p2d8fs8dn68z - -https://primal.net/e/nevent1qqsqum7j25p9z8vcyn93dsd7edx34w07eqav50qnde3vrfs466q558gdd02yr - -https://primal.net/p/nprofile1qqs06gywary09qmcp2249ztwfq3ue8wxhl2yyp3c39thzp55plvj0sgjn9mdk - -URL with a tracking parameter, no markup: -https://example.com?utm_source=newsletter1&utm_medium=email&utm_campaign=sale - -Image without markup: -https://upload.wikimedia.org/wikipedia/commons/f/f1/Heart_coraz%C3%B3n.svg - -This is an implementation of [Nostr-flavored markup](https://github.com/nostrability/nostrability/issues/146) for #gitstuff issue notes. - -You can even turn Alexandria URLs into embedded events, if they have hexids or bech32 addresses: -https://next-alexandria.gitcitadel.eu/events?id=nevent1qqstjcyerjx4laxlxc70cwzuxf3u9kkzuhdhgtu8pwrzvh7k5d5zdngpzemhxue69uhhyetvv9ujumn0wd68ytnzv9hxgq3qm3xdppkd0njmrqe2ma8a6ys39zvgp5k8u22mev8xsnqp4nh80srq0ylvuw - -But not if they have d-tags: -https://next-alexandria.gitcitadel.eu/publication?d=relay-test-thecitadel-by-unknown-v-1 - -And within a markup tag: [markup link title](https://next-alexandria.gitcitadel.com/publication?id=84ad65f7a321404f55d97c2208dd3686c41724e6c347d3ee53cfe16f67cdfb7c). - -And to localhost: http://localhost:4173/publication?id=c36b54991e459221f444612d88ea94ef5bb4a1b93863ef89b1328996746f6d25 - -https://next-alexandria.gitcitadel.eu/events?id=nprofile1qqs99d9qw67th0wr5xh05de4s9k0wjvnkxudkgptq8yg83vtulad30gxyk5sf - -You can even include code inline, like `
` or - -``` -in a code block -``` - -You can even use a multi-line code block, with a json tag. - -```json -{ -"created_at":1745038670,"content":"# This is a test\n\nIt is _only_ a test. I just wanted to see if the *markup* renders correctly on the page, even if I use **two asterisks** for bold text.[^1]\n\nnpub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz.\n\n> This is important information\n\n> This is multiple\n> lines of\n> important information\n> with a second[^2] footnote.\n\n* but\n* not\n* really\n\n## More testing\n\n1. first\n2. second\n3. third\n\nHere is a horizontal rule:\n\n---\n\nThis is an implementation of [Nostr-flavored markup](github.com/nostrability/nostrability/issues/146 ) for #gitstuff issue notes.\n\nYou can even include `code inline` or\n\n```\nin a code block\n```\n\nYou can even use a \n\n```json\nmultiline of json block\n```\n\n\n![Nostr logo](https://user-images.githubusercontent.com/99301796/219900773-d6d02038-e2a0-4334-9f28-c14d40ab6fe7.png)\n\n[^1]: this is a footnote\n[^2]: so is this","tags":[["subject","test"],["alt","git repository issue: test"],["a","30617:fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1:Alexandria","","root"],["p","fd208ee8c8f283780a9552896e4823cc9dc6bfd442063889577106940fd927c1"],["t","gitstuff"]],"kind":1621,"pubkey":"dd664d5e4016433a8cd69f005ae1480804351789b59de5af06276de65633d319","id":"e78a689369511fdb3c36b990380c2d8db2b5e62f13f6b836e93ef5a09611afe8","sig":"7a2b3a6f6f61b6ea04de1fe873e46d40f2a220f02cdae004342430aa1df67647a9589459382f22576c651b3d09811546bbd79564cf472deaff032f137e94a865" -} -``` - -C or C++: -```cpp -bool getBit(int num, int i) { - return ((num & (1< Block quotes are -> written like so. -> -> They can span multiple paragraphs, -> if you like. -``` - -Test out some emojis :heart: and :trophy: - -#### Here is an image![^some reference text] - -![Nostr logo](https://user-images.githubusercontent.com/99301796/219900773-d6d02038-e2a0-4334-9f28-c14d40ab6fe7.png) - -### I went ahead and implemented tables, too. - -A neat table[^some reference text]: - -| Syntax | Description | -| ----------- | ----------- | -| Header | Title | -| Paragraph | Text | - -A messy table (should render the same as above): - -| Syntax | Description | -| --- | ----------- | -| Header | Title | -| Paragraph | Text | - -Here is a table without a header row: - -| Sometimes | you don't | -| need a | header | -| just | pipes | - -[^1]: this is a footnote -[^some reference text]: this is a footnote that isn't a number \ No newline at end of file diff --git a/tests/unit/advancedMarkupParser.test.ts b/tests/unit/advancedMarkupParser.test.ts deleted file mode 100644 index 0d868d1..0000000 --- a/tests/unit/advancedMarkupParser.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseAdvancedmarkup } from '../../src/lib/utils/markup/advancedMarkupParser'; - -function stripWS(str: string) { - return str.replace(/\s+/g, ' ').trim(); -} - -describe('Advanced Markup Parser', () => { - it('parses headers (ATX and Setext)', async () => { - const input = '# H1\nText\n\nH2\n====\n'; - const output = await parseAdvancedmarkup(input); - expect(stripWS(output)).toContain('H1'); - expect(stripWS(output)).toContain('H2'); - }); - - it('parses bold, italic, and strikethrough', async () => { - const input = '*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain('bold'); - expect(output).toContain('italic'); - expect(output).toContain('strikethrough'); - }); - - it('parses blockquotes', async () => { - const input = '> quote'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '> quote\n> quote'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '* a\n* b'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '1. one\n2. two'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '[link](https://example.com) ![alt](https://img.com/x.png)'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '#hashtag'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain('text-primary-600'); - expect(output).toContain('#hashtag'); - }); - - it('parses nostr identifiers', async () => { - const input = 'npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain('./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); - }); - - it('parses emoji shortcodes', async () => { - const input = 'hello :smile:'; - const output = await parseAdvancedmarkup(input); - expect(output).toMatch(/😄|:smile:/); - }); - - it('parses wikilinks', async () => { - const input = '[[Test Page|display]]'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain('wikilink'); - expect(output).toContain('display'); - }); - - it('parses tables (with and without headers)', async () => { - const input = `| Syntax | Description |\n|--------|-------------|\n| Header | Title |\n| Paragraph | Text |\n\n| a | b |\n| c | d |`; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = '```js\nconsole.log(1);\n```\n```\nno lang\n```'; - const output = await parseAdvancedmarkup(input); - const textOnly = output.replace(/<[^>]+>/g, ''); - expect(output).toContain(' { - const input = '---'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain(' { - const input = 'Here is a footnote[^1].\n\n[^1]: This is the footnote.'; - const output = await parseAdvancedmarkup(input); - expect(output).toContain('Footnotes'); - expect(output).toContain('This is the footnote'); - expect(output).toContain('fn-1'); - }); -}); \ No newline at end of file diff --git a/tests/unit/basicMarkupParser.test.ts b/tests/unit/basicMarkupParser.test.ts deleted file mode 100644 index 4025b65..0000000 --- a/tests/unit/basicMarkupParser.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { parseBasicmarkup } from '../../src/lib/utils/markup/basicMarkupParser'; - -// Helper to strip whitespace for easier comparison -function stripWS(str: string) { - return str.replace(/\s+/g, ' ').trim(); -} - -describe('Basic Markup Parser', () => { - it('parses ATX and Setext headers', async () => { - const input = '# H1\nText\n\nH2\n====\n'; - const output = await parseBasicmarkup(input); - expect(stripWS(output)).toContain('H1'); - expect(stripWS(output)).toContain('H2'); - }); - - it('parses bold, italic, and strikethrough', async () => { - const input = '*bold* **bold** _italic_ __italic__ ~strikethrough~ ~~strikethrough~~'; - const output = await parseBasicmarkup(input); - expect(output).toContain('bold'); - expect(output).toContain('italic'); - expect(output).toContain('strikethrough'); - }); - - it('parses blockquotes', async () => { - const input = '> quote'; - const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '> quote\n> quote'; - const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '* a\n* b'; - const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '1. one\n2. two'; - const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '[link](https://example.com) ![alt](https://img.com/x.png)'; - const output = await parseBasicmarkup(input); - expect(output).toContain(' { - const input = '#hashtag'; - const output = await parseBasicmarkup(input); - expect(output).toContain('text-primary-600'); - expect(output).toContain('#hashtag'); - }); - - it('parses nostr identifiers', async () => { - const input = 'npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; - const output = await parseBasicmarkup(input); - expect(output).toContain('./events?id=npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); - }); - - it('parses emoji shortcodes', async () => { - const input = 'hello :smile:'; - const output = await parseBasicmarkup(input); - expect(output).toMatch(/😄|:smile:/); - }); - - it('parses wikilinks', async () => { - const input = '[[Test Page|display]]'; - const output = await parseBasicmarkup(input); - expect(output).toContain('wikilink'); - expect(output).toContain('display'); - }); -}); \ No newline at end of file diff --git a/tests/unit/coordinateDeduplication.test.ts b/tests/unit/coordinateDeduplication.test.ts deleted file mode 100644 index edbd625..0000000 --- a/tests/unit/coordinateDeduplication.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { NDKEvent } from '@nostr-dev-kit/ndk'; -import { - createCoordinateMap, - extractCoordinateFromATag, - initializeGraphState -} from '$lib/navigator/EventNetwork/utils/networkBuilder'; - -// Mock NDKEvent -class MockNDKEvent implements Partial { - id: string; - pubkey: string; - created_at?: number; - kind?: number; - content?: string; - tags: string[][]; - - constructor(params: { id: string; pubkey: string; created_at?: number; kind?: number; content?: string; tags?: string[][] }) { - this.id = params.id; - this.pubkey = params.pubkey; - this.created_at = params.created_at; - this.kind = params.kind; - this.content = params.content || ''; - this.tags = params.tags || []; - } - - getMatchingTags(tagName: string): string[][] { - return this.tags.filter(tag => tag[0] === tagName); - } -} - -// Generate a valid 64-character hex pubkey -function generatePubkey(seed: string): string { - return seed.padEnd(64, '0'); -} - -// Generate a valid 64-character hex event ID -function generateEventId(seed: string): string { - return seed.padEnd(64, '0'); -} - -describe('Coordinate-based Deduplication', () => { - // Helper to create a mock event with valid IDs - function createMockEvent(params: { - id: string; - pubkey: string; - kind?: number; - created_at?: number; - tags?: string[][]; - content?: string; - }) { - return new MockNDKEvent({ - ...params, - id: generateEventId(params.id), - pubkey: generatePubkey(params.pubkey) - }) as NDKEvent; - } - describe('createCoordinateMap', () => { - it('should create empty map for non-replaceable events', () => { - const events = [ - new MockNDKEvent({ id: '1', pubkey: generatePubkey('pubkey1'), kind: 1 }), - new MockNDKEvent({ id: '2', pubkey: generatePubkey('pubkey2'), kind: 4 }), - new MockNDKEvent({ id: '3', pubkey: generatePubkey('pubkey3'), kind: 7 }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(0); - }); - - it('should map replaceable events by coordinate', () => { - const events = [ - new MockNDKEvent({ - id: 'event1', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'publication1']] - }), - new MockNDKEvent({ - id: 'event2', - pubkey: generatePubkey('author2'), - kind: 30041, - created_at: 1001, - tags: [['d', 'section1']] - }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(2); - expect(coordinateMap.get(`30040:${generatePubkey('author1')}:publication1`)?.id).toBe('event1'); - expect(coordinateMap.get(`30041:${generatePubkey('author2')}:section1`)?.id).toBe('event2'); - }); - - it('should keep only the most recent version of duplicate coordinates', () => { - const events = [ - new MockNDKEvent({ - id: 'old_event', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'publication1']] - }), - new MockNDKEvent({ - id: 'new_event', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 2000, - tags: [['d', 'publication1']] - }), - new MockNDKEvent({ - id: 'older_event', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 500, - tags: [['d', 'publication1']] - }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(1); - expect(coordinateMap.get(`30040:${generatePubkey('author1')}:publication1`)?.id).toBe('new_event'); - }); - - it('should handle missing d-tags gracefully', () => { - const events = [ - new MockNDKEvent({ - id: 'event1', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [] - }), - new MockNDKEvent({ - id: 'event2', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 2000, - tags: [['d', 'publication1']] - }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(1); - expect(coordinateMap.get(`30040:${generatePubkey('author1')}:publication1`)?.id).toBe('event2'); - }); - - it('should handle d-tags containing colons', () => { - const events = [ - new MockNDKEvent({ - id: 'event1', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'namespace:identifier:version']] - }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(1); - expect(coordinateMap.get(`30040:${generatePubkey('author1')}:namespace:identifier:version`)?.id).toBe('event1'); - }); - - it('should handle events without timestamps', () => { - const events = [ - new MockNDKEvent({ - id: 'event_with_time', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'publication1']] - }), - new MockNDKEvent({ - id: 'event_no_time', - pubkey: generatePubkey('author1'), - kind: 30040, - tags: [['d', 'publication1']] - }) - ] as NDKEvent[]; - - const coordinateMap = createCoordinateMap(events); - expect(coordinateMap.size).toBe(1); - // Should keep the one with timestamp - expect(coordinateMap.get(`30040:${generatePubkey('author1')}:publication1`)?.id).toBe('event_with_time'); - }); - }); - - describe('extractCoordinateFromATag', () => { - it('should extract valid coordinates from a-tags', () => { - const tag = ['a', `30040:${generatePubkey('pubkey123')}:dtag123`]; - const result = extractCoordinateFromATag(tag); - - expect(result).toEqual({ - kind: 30040, - pubkey: generatePubkey('pubkey123'), - dTag: 'dtag123' - }); - }); - - it('should handle d-tags with colons', () => { - const tag = ['a', `30040:${generatePubkey('pubkey123')}:namespace:identifier:version`]; - const result = extractCoordinateFromATag(tag); - - expect(result).toEqual({ - kind: 30040, - pubkey: generatePubkey('pubkey123'), - dTag: 'namespace:identifier:version' - }); - }); - - it('should return null for invalid a-tags', () => { - expect(extractCoordinateFromATag(['a'])).toBeNull(); - expect(extractCoordinateFromATag(['a', ''])).toBeNull(); - expect(extractCoordinateFromATag(['a', 'invalid'])).toBeNull(); - expect(extractCoordinateFromATag(['a', 'invalid:format'])).toBeNull(); - expect(extractCoordinateFromATag(['a', 'notanumber:pubkey:dtag'])).toBeNull(); - }); - }); - - describe('initializeGraphState deduplication', () => { - it('should create only one node per coordinate for replaceable events', () => { - const events = [ - new MockNDKEvent({ - id: 'old_version', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'publication1'], ['title', 'Old Title']] - }), - new MockNDKEvent({ - id: 'new_version', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 2000, - tags: [['d', 'publication1'], ['title', 'New Title']] - }), - new MockNDKEvent({ - id: 'different_pub', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1500, - tags: [['d', 'publication2'], ['title', 'Different Publication']] - }) - ] as NDKEvent[]; - - const graphState = initializeGraphState(events); - - // Should have only 2 nodes (one for each unique coordinate) - expect(graphState.nodeMap.size).toBe(2); - expect(graphState.nodeMap.has('new_version')).toBe(true); - expect(graphState.nodeMap.has('different_pub')).toBe(true); - expect(graphState.nodeMap.has('old_version')).toBe(false); - }); - - it('should handle mix of replaceable and non-replaceable events', () => { - const events = [ - new MockNDKEvent({ - id: 'regular1', - pubkey: generatePubkey('author1'), - kind: 1, - created_at: 1000 - }), - new MockNDKEvent({ - id: 'regular2', - pubkey: generatePubkey('author1'), - kind: 1, - created_at: 2000 - }), - new MockNDKEvent({ - id: 'replaceable1', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'publication1']] - }), - new MockNDKEvent({ - id: 'replaceable2', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 2000, - tags: [['d', 'publication1']] - }) - ] as NDKEvent[]; - - const graphState = initializeGraphState(events); - - // Should have 3 nodes: 2 regular events + 1 replaceable (latest version) - expect(graphState.nodeMap.size).toBe(3); - expect(graphState.nodeMap.has('regular1')).toBe(true); - expect(graphState.nodeMap.has('regular2')).toBe(true); - expect(graphState.nodeMap.has('replaceable2')).toBe(true); - expect(graphState.nodeMap.has('replaceable1')).toBe(false); - }); - - it('should correctly handle referenced events with coordinates', () => { - const events = [ - new MockNDKEvent({ - id: 'index_old', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', 'book1'], ['title', 'Old Book Title']] - }), - new MockNDKEvent({ - id: 'index_new', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 2000, - tags: [['d', 'book1'], ['title', 'New Book Title']] - }), - new MockNDKEvent({ - id: 'chapter1', - pubkey: generatePubkey('author1'), - kind: 30041, - created_at: 1500, - tags: [ - ['d', 'chapter1'], - ['a', `30040:${generatePubkey('author1')}:book1`, 'relay1'] - ] - }) - ] as NDKEvent[]; - - const graphState = initializeGraphState(events); - - // Only the new version of the index should be referenced - expect(graphState.referencedIds.has('index_new')).toBe(true); - expect(graphState.referencedIds.has('index_old')).toBe(false); - }); - - it('should handle edge cases in coordinate generation', () => { - const events = [ - // Event with empty d-tag - new MockNDKEvent({ - id: 'empty_dtag', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1000, - tags: [['d', '']] - }), - // Event with no d-tag - new MockNDKEvent({ - id: 'no_dtag', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1001, - tags: [] - }), - // Event with special characters in d-tag - new MockNDKEvent({ - id: 'special_chars', - pubkey: generatePubkey('author1'), - kind: 30040, - created_at: 1002, - tags: [['d', 'test/path:to@file.txt']] - }), - // Non-replaceable event (should always be included) - new MockNDKEvent({ - id: 'non_replaceable', - pubkey: generatePubkey('author1'), - kind: 1, - created_at: 1003 - }) - ] as NDKEvent[]; - - const graphState = initializeGraphState(events); - - // Empty d-tag should create a valid coordinate - expect(graphState.nodeMap.has('empty_dtag')).toBe(true); - // No d-tag means no coordinate, but event is still included (not replaceable without coordinate) - expect(graphState.nodeMap.has('no_dtag')).toBe(true); - // Special characters should be preserved - expect(graphState.nodeMap.has('special_chars')).toBe(true); - // Non-replaceable should always be included - expect(graphState.nodeMap.has('non_replaceable')).toBe(true); - }); - }); -}); \ No newline at end of file diff --git a/tests/unit/linkRenderingDebug.test.ts b/tests/unit/linkRenderingDebug.test.ts deleted file mode 100644 index 652155b..0000000 --- a/tests/unit/linkRenderingDebug.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { generateGraph, generateStarGraph } from '$lib/navigator/EventNetwork/utils/networkBuilder'; -import { enhanceGraphWithTags } from '$lib/navigator/EventNetwork/utils/tagNetworkBuilder'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; - -// Mock NDKEvent -function createMockEvent(id: string, kind: number, tags: string[][] = []): NDKEvent { - return { - id, - kind, - pubkey: 'test-pubkey', - created_at: Date.now() / 1000, - content: `Content for ${id}`, - tags, - getMatchingTags: (tagName: string) => tags.filter(t => t[0] === tagName) - } as NDKEvent; -} - -describe('Link Rendering Debug Tests', () => { - describe('Link Generation in Graph Builders', () => { - it('should generate links in standard graph', () => { - const events = [ - createMockEvent('index1', 30040), - createMockEvent('content1', 30041, [['a', '30040:test-pubkey:index1']]), - createMockEvent('content2', 30041, [['a', '30040:test-pubkey:index1']]) - ]; - - const graph = generateGraph(events, 2); - - console.log('Standard graph:', { - nodes: graph.nodes.map(n => ({ id: n.id, type: n.type })), - links: graph.links.map(l => ({ - source: typeof l.source === 'string' ? l.source : l.source.id, - target: typeof l.target === 'string' ? l.target : l.target.id - })) - }); - - expect(graph.nodes).toHaveLength(3); - expect(graph.links).toHaveLength(2); // Two content nodes linking to index - }); - - it('should generate links in star graph', () => { - const events = [ - createMockEvent('index1', 30040), - createMockEvent('content1', 30041, [['a', '30040:test-pubkey:index1']]), - createMockEvent('content2', 30041, [['a', '30040:test-pubkey:index1']]) - ]; - - const graph = generateStarGraph(events, 2); - - console.log('Star graph:', { - nodes: graph.nodes.map(n => ({ id: n.id, type: n.type })), - links: graph.links.map(l => ({ - source: typeof l.source === 'string' ? l.source : l.source.id, - target: typeof l.target === 'string' ? l.target : l.target.id - })) - }); - - expect(graph.nodes).toHaveLength(3); - expect(graph.links).toHaveLength(2); - }); - - it('should generate links with tag anchors', () => { - const events = [ - createMockEvent('index1', 30040, [['t', 'bitcoin']]), - createMockEvent('content1', 30041, [['a', '30040:test-pubkey:index1'], ['t', 'bitcoin']]), - ]; - - const baseGraph = generateGraph(events, 2); - const enhancedGraph = enhanceGraphWithTags(baseGraph, events, 't', 1000, 600); - - console.log('Enhanced graph with tags:', { - nodes: enhancedGraph.nodes.map(n => ({ - id: n.id, - type: n.type, - isTagAnchor: n.isTagAnchor - })), - links: enhancedGraph.links.map(l => ({ - source: typeof l.source === 'string' ? l.source : l.source.id, - target: typeof l.target === 'string' ? l.target : l.target.id - })) - }); - - // Should have original nodes plus tag anchor - expect(enhancedGraph.nodes.length).toBeGreaterThan(baseGraph.nodes.length); - // Should have original links plus tag connections - expect(enhancedGraph.links.length).toBeGreaterThan(baseGraph.links.length); - }); - }); - - describe('Link Data Structure', () => { - it('should have proper source and target references', () => { - const events = [ - createMockEvent('index1', 30040), - createMockEvent('content1', 30041, [['a', '30040:test-pubkey:index1']]) - ]; - - const graph = generateGraph(events, 2); - - graph.links.forEach(link => { - expect(link.source).toBeDefined(); - expect(link.target).toBeDefined(); - - // Check if source/target are strings (IDs) or objects - if (typeof link.source === 'string') { - const sourceNode = graph.nodes.find(n => n.id === link.source); - expect(sourceNode).toBeDefined(); - } else { - expect(link.source.id).toBeDefined(); - } - - if (typeof link.target === 'string') { - const targetNode = graph.nodes.find(n => n.id === link.target); - expect(targetNode).toBeDefined(); - } else { - expect(link.target.id).toBeDefined(); - } - }); - }); - }); - - describe('D3 Force Simulation Link Format', () => { - it('should verify link format matches D3 requirements', () => { - const events = [ - createMockEvent('index1', 30040), - createMockEvent('content1', 30041, [['a', '30040:test-pubkey:index1']]) - ]; - - const graph = generateGraph(events, 2); - - // D3 expects links to have source/target that reference node objects or IDs - graph.links.forEach(link => { - // For D3, links should initially have string IDs - if (typeof link.source === 'string') { - expect(graph.nodes.some(n => n.id === link.source)).toBe(true); - } - if (typeof link.target === 'string') { - expect(graph.nodes.some(n => n.id === link.target)).toBe(true); - } - }); - }); - }); -}); \ No newline at end of file diff --git a/tests/unit/visualizationReactivity.extended.test.ts b/tests/unit/visualizationReactivity.extended.test.ts deleted file mode 100644 index fd9e8b8..0000000 --- a/tests/unit/visualizationReactivity.extended.test.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { writable, get } from 'svelte/store'; -import { tick } from 'svelte'; -import type { NDKEvent } from '@nostr-dev-kit/ndk'; - -// Mock stores and components -vi.mock('$lib/stores/visualizationConfig', () => { - const mockStore = writable({ - maxPublicationIndices: -1, - maxEventsPerIndex: -1, - searchThroughFetched: false - }); - - return { - visualizationConfig: { - subscribe: mockStore.subscribe, - setMaxPublicationIndices: vi.fn((value: number) => { - mockStore.update(s => ({ ...s, maxPublicationIndices: value })); - }), - setMaxEventsPerIndex: vi.fn((value: number) => { - mockStore.update(s => ({ ...s, maxEventsPerIndex: value })); - }), - toggleSearchThroughFetched: vi.fn(() => { - mockStore.update(s => ({ ...s, searchThroughFetched: !s.searchThroughFetched })); - }) - } - }; -}); - -vi.mock('$lib/stores/displayLimits', () => { - const mockStore = writable({ - max30040: -1, - max30041: -1, - fetchIfNotFound: false - }); - - return { - displayLimits: mockStore - }; -}); - -describe('Extended Visualization Reactivity Tests', () => { - let updateCount = 0; - let lastUpdateType: string | null = null; - let simulationRestarts = 0; - - // Mock updateGraph function - const mockUpdateGraph = vi.fn((type: string) => { - updateCount++; - lastUpdateType = type; - }); - - // Mock simulation restart - const mockRestartSimulation = vi.fn(() => { - simulationRestarts++; - }); - - beforeEach(() => { - updateCount = 0; - lastUpdateType = null; - simulationRestarts = 0; - vi.clearAllMocks(); - }); - - describe('Parameter Update Paths', () => { - it('should trigger data fetch for networkFetchLimit changes', async () => { - const params = { - networkFetchLimit: 50, - levelsToRender: 2, - showTagAnchors: false, - starVisualization: false, - tagExpansionDepth: 0 - }; - - // Change networkFetchLimit - const oldParams = { ...params }; - params.networkFetchLimit = 100; - - const needsFetch = params.networkFetchLimit !== oldParams.networkFetchLimit; - expect(needsFetch).toBe(true); - - if (needsFetch) { - mockUpdateGraph('fetch-required'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('fetch-required'); - expect(lastUpdateType).toBe('fetch-required'); - }); - - it('should trigger data fetch for levelsToRender changes', async () => { - const params = { - networkFetchLimit: 50, - levelsToRender: 2, - showTagAnchors: false, - starVisualization: false, - tagExpansionDepth: 0 - }; - - // Change levelsToRender - const oldParams = { ...params }; - params.levelsToRender = 3; - - const needsFetch = params.levelsToRender !== oldParams.levelsToRender; - expect(needsFetch).toBe(true); - - if (needsFetch) { - mockUpdateGraph('fetch-required'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('fetch-required'); - }); - - it('should trigger fetch for tagExpansionDepth when > 0', async () => { - const params = { - tagExpansionDepth: 0, - showTagAnchors: true - }; - - // Change to depth > 0 - const oldParams = { ...params }; - params.tagExpansionDepth = 1; - - const needsFetch = params.tagExpansionDepth > 0 && - params.tagExpansionDepth !== oldParams.tagExpansionDepth; - expect(needsFetch).toBe(true); - - if (needsFetch) { - mockUpdateGraph('tag-expansion-fetch'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('tag-expansion-fetch'); - }); - - it('should not trigger fetch for tagExpansionDepth = 0', async () => { - const params = { - tagExpansionDepth: 2, - showTagAnchors: true - }; - - // Change to depth = 0 - const oldParams = { ...params }; - params.tagExpansionDepth = 0; - - const needsFetch = params.tagExpansionDepth > 0; - expect(needsFetch).toBe(false); - - if (!needsFetch) { - mockUpdateGraph('visual-only'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('visual-only'); - }); - - it('should handle visual-only parameter changes', async () => { - const visualParams = [ - { param: 'showTagAnchors', oldValue: false, newValue: true }, - { param: 'starVisualization', oldValue: false, newValue: true }, - { param: 'selectedTagType', oldValue: 't', newValue: 'p' } - ]; - - visualParams.forEach(({ param, oldValue, newValue }) => { - vi.clearAllMocks(); - - const needsFetch = false; // Visual parameters never need fetch - if (!needsFetch) { - mockUpdateGraph('visual-only'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('visual-only'); - expect(mockUpdateGraph).toHaveBeenCalledTimes(1); - }); - }); - }); - - describe('Display Limits Integration', () => { - it('should handle maxPublicationIndices changes', async () => { - const { visualizationConfig } = await import('$lib/stores/visualizationConfig'); - const { displayLimits } = await import('$lib/stores/displayLimits'); - - let configValue: any; - const unsubscribe = visualizationConfig.subscribe(v => configValue = v); - - // Set new limit - visualizationConfig.setMaxPublicationIndices(10); - await tick(); - - expect(configValue.maxPublicationIndices).toBe(10); - - // This should trigger a visual update (filtering existing data) - mockUpdateGraph('filter-existing'); - expect(mockUpdateGraph).toHaveBeenCalledWith('filter-existing'); - - unsubscribe(); - }); - - it('should handle unlimited (-1) values correctly', async () => { - const { displayLimits } = await import('$lib/stores/displayLimits'); - - let limitsValue: any; - const unsubscribe = displayLimits.subscribe(v => limitsValue = v); - - // Set to unlimited - displayLimits.update(limits => ({ - ...limits, - max30040: -1, - max30041: -1 - })); - await tick(); - - expect(limitsValue.max30040).toBe(-1); - expect(limitsValue.max30041).toBe(-1); - - // Unlimited should show all events - const shouldFilter = limitsValue.max30040 !== -1 || limitsValue.max30041 !== -1; - expect(shouldFilter).toBe(false); - - unsubscribe(); - }); - - it('should handle fetchIfNotFound toggle', async () => { - const { displayLimits } = await import('$lib/stores/displayLimits'); - - let limitsValue: any; - const unsubscribe = displayLimits.subscribe(v => limitsValue = v); - - // Toggle fetchIfNotFound - displayLimits.update(limits => ({ - ...limits, - fetchIfNotFound: true - })); - await tick(); - - expect(limitsValue.fetchIfNotFound).toBe(true); - - // This should potentially trigger fetches for missing events - if (limitsValue.fetchIfNotFound) { - mockUpdateGraph('fetch-missing'); - } - - expect(mockUpdateGraph).toHaveBeenCalledWith('fetch-missing'); - - unsubscribe(); - }); - }); - - describe('State Synchronization', () => { - it('should maintain consistency between related parameters', async () => { - let showTagAnchors = false; - let tagExpansionDepth = 2; - let selectedTagType = 't'; - - // When disabling tag anchors, depth should reset - showTagAnchors = false; - if (!showTagAnchors && tagExpansionDepth > 0) { - tagExpansionDepth = 0; - } - - expect(tagExpansionDepth).toBe(0); - - // When enabling tag anchors, previous values can be restored - showTagAnchors = true; - // selectedTagType should remain unchanged - expect(selectedTagType).toBe('t'); - }); - - it('should handle disabled tags state updates', async () => { - const disabledTags = new Set(); - const tagAnchors = [ - { id: 't-bitcoin', type: 't', label: 'bitcoin' }, - { id: 't-nostr', type: 't', label: 'nostr' } - ]; - - // Toggle tag state - const tagId = 't-bitcoin'; - if (disabledTags.has(tagId)) { - disabledTags.delete(tagId); - } else { - disabledTags.add(tagId); - } - - expect(disabledTags.has('t-bitcoin')).toBe(true); - expect(disabledTags.has('t-nostr')).toBe(false); - - // Visual update only - mockUpdateGraph('tag-filter'); - expect(mockUpdateGraph).toHaveBeenCalledWith('tag-filter'); - }); - }); - - describe('Performance and Memory Management', () => { - it('should debounce rapid parameter changes', async () => { - const debounceDelay = 100; - let pendingUpdate: any = null; - let updateTimer: any = null; - - const debouncedUpdate = (type: string) => { - if (updateTimer) clearTimeout(updateTimer); - - pendingUpdate = type; - updateTimer = setTimeout(() => { - mockUpdateGraph(pendingUpdate); - pendingUpdate = null; - }, debounceDelay); - }; - - // Rapid changes - debouncedUpdate('change1'); - debouncedUpdate('change2'); - debouncedUpdate('change3'); - - // Should not have called update yet - expect(mockUpdateGraph).not.toHaveBeenCalled(); - - // Wait for debounce - await new Promise(resolve => setTimeout(resolve, debounceDelay + 10)); - - // Should only call once with last change - expect(mockUpdateGraph).toHaveBeenCalledTimes(1); - expect(mockUpdateGraph).toHaveBeenCalledWith('change3'); - }); - - it('should clean up position cache for removed nodes', () => { - const positionCache = new Map(); - const maxCacheSize = 1000; - - // Add positions - for (let i = 0; i < 1500; i++) { - positionCache.set(`node${i}`, { x: i * 10, y: i * 10 }); - } - - // Clean up old entries if cache too large - if (positionCache.size > maxCacheSize) { - const entriesToKeep = Array.from(positionCache.entries()) - .slice(-maxCacheSize); - positionCache.clear(); - entriesToKeep.forEach(([k, v]) => positionCache.set(k, v)); - } - - expect(positionCache.size).toBe(maxCacheSize); - }); - - it('should restart simulation efficiently', () => { - const needsSimulationRestart = (paramChanged: string) => { - const restartParams = ['starVisualization', 'showTagAnchors']; - return restartParams.includes(paramChanged); - }; - - // Test various parameter changes - expect(needsSimulationRestart('starVisualization')).toBe(true); - expect(needsSimulationRestart('showTagAnchors')).toBe(true); - expect(needsSimulationRestart('selectedTagType')).toBe(false); - - // Only restart when necessary - if (needsSimulationRestart('starVisualization')) { - mockRestartSimulation(); - } - - expect(mockRestartSimulation).toHaveBeenCalledTimes(1); - }); - }); - - describe('Edge Cases and Error Handling', () => { - it('should handle empty event arrays gracefully', () => { - const events: NDKEvent[] = []; - const graph = { nodes: [], links: [] }; - - // Should not crash with empty data - expect(() => { - if (events.length === 0) { - mockUpdateGraph('empty-data'); - } - }).not.toThrow(); - - expect(mockUpdateGraph).toHaveBeenCalledWith('empty-data'); - }); - - it('should handle parameter validation', () => { - const validateParams = (params: any) => { - const errors: string[] = []; - - if (params.networkFetchLimit < 1) { - errors.push('networkFetchLimit must be >= 1'); - } - if (params.levelsToRender < 0) { - errors.push('levelsToRender must be >= 0'); - } - if (params.tagExpansionDepth < 0 || params.tagExpansionDepth > 10) { - errors.push('tagExpansionDepth must be between 0 and 10'); - } - - return errors; - }; - - const invalidParams = { - networkFetchLimit: 0, - levelsToRender: -1, - tagExpansionDepth: 15 - }; - - const errors = validateParams(invalidParams); - expect(errors).toHaveLength(3); - expect(errors).toContain('networkFetchLimit must be >= 1'); - }); - - it('should handle concurrent updates safely', async () => { - let isUpdating = false; - const updates: string[] = []; - - const safeUpdate = async (type: string) => { - if (isUpdating) { - // Queue update - return new Promise(resolve => { - setTimeout(() => safeUpdate(type).then(resolve), 10); - }); - } - - isUpdating = true; - updates.push(type); - await new Promise(resolve => setTimeout(resolve, 50)); - isUpdating = false; - }; - - // Trigger concurrent updates - const promises = [ - safeUpdate('update1'), - safeUpdate('update2'), - safeUpdate('update3') - ]; - - await Promise.all(promises); - - // All updates should complete - expect(updates).toHaveLength(3); - }); - }); -}); \ No newline at end of file