Browse Source

Cleanup and redundancy removal

master
limina1 9 months ago
parent
commit
750b143dcd
  1. 18
      .gitignore
  2. 105
      docs/event-types-panel-redesign.org
  3. 332
      docs/mini-projects/08-visualization-optimization-implementation.md
  4. 124
      docs/mini-projects/08-visualization-optimization-quick-reference.md
  5. 168
      docs/mini-projects/08-visualization-optimization-summary.md
  6. 2
      src/lib/components/EventTypeConfig.svelte
  7. 1
      src/lib/navigator/EventNetwork/Legend.svelte
  8. 50
      src/lib/navigator/EventNetwork/Settings.svelte
  9. 29
      src/lib/navigator/EventNetwork/index.svelte
  10. 41
      src/lib/navigator/EventNetwork/utils/common.ts
  11. 12
      src/lib/navigator/EventNetwork/utils/forceSimulation.ts
  12. 12
      src/lib/navigator/EventNetwork/utils/networkBuilder.ts
  13. 36
      src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts
  14. 12
      src/lib/navigator/EventNetwork/utils/starNetworkBuilder.ts
  15. 35
      src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts
  16. 19
      src/lib/stores/displayLimits.ts
  17. 123
      src/lib/stores/visualizationConfig.ts
  18. 62
      src/lib/utils/displayLimits.ts
  19. 10
      src/lib/utils/eventColors.ts
  20. 108
      src/routes/visualize/+page.svelte
  21. 279
      tests/e2e/collapsible-sections.pw.spec.ts
  22. 18
      tests/e2e/example.pw.spec.ts
  23. 365
      tests/e2e/poc-performance-validation.pw.spec.ts
  24. 308
      tests/e2e/tag-anchor-interactions.pw.spec.ts
  25. 150
      tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md
  26. 150
      tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md
  27. 150
      tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md
  28. 150
      tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md
  29. 150
      tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md
  30. 150
      tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md
  31. 150
      tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md
  32. 382
      tests/integration/displayLimitsIntegration.test.ts
  33. 99
      tests/integration/markupIntegration.test.ts
  34. 244
      tests/integration/markupTestfile.md
  35. 118
      tests/unit/advancedMarkupParser.test.ts
  36. 88
      tests/unit/basicMarkupParser.test.ts
  37. 376
      tests/unit/coordinateDeduplication.test.ts
  38. 143
      tests/unit/linkRenderingDebug.test.ts
  39. 436
      tests/unit/visualizationReactivity.extended.test.ts

18
.gitignore vendored

@ -9,9 +9,21 @@ node_modules
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
# tests # tests - ignore all test directories and files
/tests/e2e/html-report/*.html /tests/
/tests/e2e/test-results/*.last-run.json /test/
/__tests__/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
*.test.svelte
*.spec.svelte
/coverage/
/.nyc_output/
# documentation
/docs/
# Deno # Deno
/.deno/ /.deno/

105
docs/event-types-panel-redesign.org

@ -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<Record<string, boolean>>({});
let disabledPersonsMap = $state<Record<string, boolean>>({});
// 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

332
docs/mini-projects/08-visualization-optimization-implementation.md

@ -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<string> }
| { kind: 'visual'; params: Set<string> };
```
### 2. Parameter Tracking
Track current and previous parameter values to detect changes:
```typescript
let lastUpdateParams = $state<UpdateParams>({
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<NetworkNode[]>([]);
let links = $state<NetworkLink[]>([]);
```
## 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<string> {
const changes = new Set<string>();
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<string>): 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<string>) {
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*

124
docs/mini-projects/08-visualization-optimization-quick-reference.md

@ -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*

168
docs/mini-projects/08-visualization-optimization-summary.md

@ -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<string> }
| { kind: 'visual'; params: Set<string> };
```
### 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*

2
src/lib/components/EventTypeConfig.svelte

@ -102,7 +102,7 @@
<div class="space-y-2"> <div class="space-y-2">
{#each $visualizationConfig.eventConfigs as config} {#each $visualizationConfig.eventConfigs as config}
{@const isLoaded = (eventCounts[config.kind] || 0) > 0} {@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 color = getEventKindColor(config.kind)}
{@const borderColor = isLoaded ? 'border-green-500' : 'border-red-500'} {@const borderColor = isLoaded ? 'border-green-500' : 'border-red-500'}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

1
src/lib/navigator/EventNetwork/Legend.svelte

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { Button } from "flowbite-svelte";
import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons"; import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons";
import { getEventKindColor, getEventKindName } from '$lib/utils/eventColors'; import { getEventKindColor, getEventKindName } from '$lib/utils/eventColors';

50
src/lib/navigator/EventNetwork/Settings.svelte

@ -1,8 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Button } from "flowbite-svelte";
import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons"; import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons";
import { fly } from "svelte/transition";
import { quintOut } from "svelte/easing";
import EventTypeConfig from "$lib/components/EventTypeConfig.svelte"; import EventTypeConfig from "$lib/components/EventTypeConfig.svelte";
import { visualizationConfig } from "$lib/stores/visualizationConfig"; import { visualizationConfig } from "$lib/stores/visualizationConfig";
import { Toggle } from "flowbite-svelte"; import { Toggle } from "flowbite-svelte";
@ -13,7 +10,6 @@
onupdate, onupdate,
onclear = () => {}, onclear = () => {},
starVisualization = $bindable(true), starVisualization = $bindable(true),
onFetchMissing = () => {},
eventCounts = {}, eventCounts = {},
profileStats = { totalFetched: 0, displayLimit: 50 }, profileStats = { totalFetched: 0, displayLimit: 50 },
} = $props<{ } = $props<{
@ -23,7 +19,6 @@
onclear?: () => void; onclear?: () => void;
starVisualization?: boolean; starVisualization?: boolean;
onFetchMissing?: (ids: string[]) => void;
eventCounts?: { [kind: number]: number }; eventCounts?: { [kind: number]: number };
profileStats?: { totalFetched: number; displayLimit: number }; profileStats?: { totalFetched: number; displayLimit: number };
}>(); }>();
@ -43,12 +38,6 @@
function toggleVisualSettings() { function toggleVisualSettings() {
visualSettingsExpanded = !visualSettingsExpanded; visualSettingsExpanded = !visualSettingsExpanded;
} }
/**
* Handles updates to visualization settings
*/
function handleLimitUpdate() {
onupdate();
}
</script> </script>
<div class="leather-legend sm:!right-1 sm:!left-auto"> <div class="leather-legend sm:!right-1 sm:!left-auto">
@ -102,24 +91,27 @@
</div> </div>
{#if visualSettingsExpanded} {#if visualSettingsExpanded}
<div class="space-y-2"> <div class="space-y-4">
<label <div class="space-y-2">
class="leather bg-transparent legend-text flex items-center space-x-2" <label
> class="leather bg-transparent legend-text flex items-center space-x-2"
<Toggle >
checked={starVisualization} <Toggle
onchange={(e: Event) => { checked={starVisualization}
const target = e.target as HTMLInputElement; onchange={(e: Event) => {
starVisualization = target.checked; const target = e.target as HTMLInputElement;
}} starVisualization = target.checked;
class="text-xs" }}
/> class="text-xs"
<span>Star Network View</span> />
</label> <span>Star Network View</span>
<p class="text-xs text-gray-500 dark:text-gray-400"> </label>
Toggle between star clusters (on) and linear sequence (off) <p class="text-xs text-gray-500 dark:text-gray-400">
visualization Toggle between star clusters (on) and linear sequence (off)
</p> visualization
</p>
</div>
</div> </div>
{/if} {/if}

29
src/lib/navigator/EventNetwork/index.svelte

@ -74,7 +74,6 @@
onupdate, onupdate,
onclear = () => {}, onclear = () => {},
onTagExpansionChange, onTagExpansionChange,
onFetchMissing = () => {},
profileStats = { totalFetched: 0, displayLimit: 50 }, profileStats = { totalFetched: 0, displayLimit: 50 },
allEventCounts = {} allEventCounts = {}
} = $props<{ } = $props<{
@ -84,7 +83,6 @@
onupdate: () => void; onupdate: () => void;
onclear?: () => void; onclear?: () => void;
onTagExpansionChange?: (depth: number, tags: string[]) => void; onTagExpansionChange?: (depth: number, tags: string[]) => void;
onFetchMissing?: (ids: string[]) => void;
profileStats?: { totalFetched: number; displayLimit: number }; profileStats?: { totalFetched: number; displayLimit: number };
allEventCounts?: { [kind: number]: number }; allEventCounts?: { [kind: number]: number };
}>(); }>();
@ -134,7 +132,6 @@
let showTagAnchors = $state(false); let showTagAnchors = $state(false);
let selectedTagType = $state("t"); // Default to hashtags let selectedTagType = $state("t"); // Default to hashtags
let tagAnchorInfo = $state<any[]>([]); let tagAnchorInfo = $state<any[]>([]);
let tagExpansionDepth = $state(0); // Default to no expansion
// Store initial state to detect if component is being recreated // Store initial state to detect if component is being recreated
let componentId = Math.random(); let componentId = Math.random();
@ -171,21 +168,6 @@
let displayedPersonCount = $state(0); let displayedPersonCount = $state(0);
let hasInitializedPersons = $state(false); 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 // Update dimensions when container changes
$effect(() => { $effect(() => {
@ -1001,7 +983,6 @@
}); });
// Track previous values to avoid unnecessary calls // Track previous values to avoid unnecessary calls
let previousDepth = $state(0);
let previousTagType = $state(selectedTagType); let previousTagType = $state(selectedTagType);
let isInitialized = $state(false); let isInitialized = $state(false);
@ -1020,12 +1001,10 @@
if (!isInitialized || !onTagExpansionChange) return; if (!isInitialized || !onTagExpansionChange) return;
// Check if we need to trigger expansion // Check if we need to trigger expansion
const depthChanged = tagExpansionDepth !== previousDepth;
const tagTypeChanged = selectedTagType !== previousTagType; const tagTypeChanged = selectedTagType !== previousTagType;
const shouldExpand = showTagAnchors && (depthChanged || tagTypeChanged); const shouldExpand = showTagAnchors && tagTypeChanged;
if (shouldExpand) { if (shouldExpand) {
previousDepth = tagExpansionDepth;
previousTagType = selectedTagType; previousTagType = selectedTagType;
// Extract unique tags from current events // Extract unique tags from current events
@ -1038,14 +1017,12 @@
}); });
debug("Tag expansion requested", { debug("Tag expansion requested", {
depth: tagExpansionDepth,
tagType: selectedTagType, tagType: selectedTagType,
tags: Array.from(tags), tags: Array.from(tags),
depthChanged,
tagTypeChanged tagTypeChanged
}); });
onTagExpansionChange(tagExpansionDepth, Array.from(tags)); onTagExpansionChange(0, Array.from(tags));
} }
}); });
@ -1221,7 +1198,6 @@
{autoDisabledTags} {autoDisabledTags}
bind:showTagAnchors bind:showTagAnchors
bind:selectedTagType bind:selectedTagType
bind:tagExpansionDepth
onTagSettingsChange={() => { onTagSettingsChange={() => {
// Trigger graph update when tag settings change // Trigger graph update when tag settings change
if (svg && events?.length) { if (svg && events?.length) {
@ -1250,7 +1226,6 @@
{totalCount} {totalCount}
{onupdate} {onupdate}
{onclear} {onclear}
{onFetchMissing}
bind:starVisualization bind:starVisualization
eventCounts={allEventCounts} eventCounts={allEventCounts}
{profileStats} {profileStats}

41
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);
}
};
}

12
src/lib/navigator/EventNetwork/utils/forceSimulation.ts

@ -7,20 +7,14 @@
import type { NetworkNode, NetworkLink } from "../types"; import type { NetworkNode, NetworkLink } from "../types";
import * as d3 from "d3"; import * as d3 from "d3";
import { createDebugFunction } from "./common";
// Configuration // Configuration
const DEBUG = false; // Set to true to enable debug logging
const GRAVITY_STRENGTH = 0.05; // Strength of global gravity const GRAVITY_STRENGTH = 0.05; // Strength of global gravity
const CONNECTED_GRAVITY_STRENGTH = 0.3; // Strength of gravity between connected nodes const CONNECTED_GRAVITY_STRENGTH = 0.3; // Strength of gravity between connected nodes
/** // Debug function
* Debug logging function that only logs when DEBUG is true const debug = createDebugFunction("ForceSimulation");
*/
function debug(...args: any[]) {
if (DEBUG) {
console.log("[ForceSimulation]", ...args);
}
}
/** /**
* Type definition for D3 force simulation * Type definition for D3 force simulation

12
src/lib/navigator/EventNetwork/utils/networkBuilder.ts

@ -11,20 +11,14 @@ import { nip19 } from "nostr-tools";
import { standardRelays } from "$lib/consts"; import { standardRelays } from "$lib/consts";
import { getMatchingTags } from '$lib/utils/nostrUtils'; import { getMatchingTags } from '$lib/utils/nostrUtils';
import { getDisplayNameSync } from '$lib/utils/profileCache'; import { getDisplayNameSync } from '$lib/utils/profileCache';
import { createDebugFunction } from "./common";
// Configuration // Configuration
const DEBUG = false; // Set to true to enable debug logging
const INDEX_EVENT_KIND = 30040; const INDEX_EVENT_KIND = 30040;
const CONTENT_EVENT_KIND = 30041; const CONTENT_EVENT_KIND = 30041;
/** // Debug function
* Debug logging function that only logs when DEBUG is true const debug = createDebugFunction("NetworkBuilder");
*/
function debug(...args: any[]) {
if (DEBUG) {
console.log("[NetworkBuilder]", ...args);
}
}
/** /**
* Creates a NetworkNode from an NDKEvent * Creates a NetworkNode from an NDKEvent

36
src/lib/navigator/EventNetwork/utils/personNetworkBuilder.ts

@ -7,26 +7,15 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink } from "../types"; import type { NetworkNode, NetworkLink } from "../types";
import { getDisplayNameSync } from "$lib/utils/profileCache"; import { getDisplayNameSync } from "$lib/utils/profileCache";
import { SeededRandom, createDebugFunction } from "./common";
const PERSON_ANCHOR_RADIUS = 15; const PERSON_ANCHOR_RADIUS = 15;
const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000; const PERSON_ANCHOR_PLACEMENT_RADIUS = 1000;
const MAX_PERSON_NODES = 20; // Default limit for person nodes const MAX_PERSON_NODES = 20; // Default limit for person nodes
/** // Debug function
* Simple seeded random number generator const debug = createDebugFunction("PersonNetworkBuilder");
*/
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
next(): number {
this.seed = (this.seed * 9301 + 49297) % 233280;
return this.seed / 233280;
}
}
/** /**
* Creates a deterministic seed from a string * Creates a deterministic seed from a string
@ -58,12 +47,11 @@ export function extractUniquePersons(
// Map of pubkey -> PersonConnection // Map of pubkey -> PersonConnection
const personMap = new Map<string, PersonConnection>(); const personMap = new Map<string, PersonConnection>();
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 // First collect pubkeys from follow list events
const followListPubkeys = new Set<string>(); const followListPubkeys = new Set<string>();
if (followListEvents && followListEvents.length > 0) { if (followListEvents && followListEvents.length > 0) {
console.log(`[PersonBuilder] Processing ${followListEvents.length} follow list events`);
followListEvents.forEach((event) => { followListEvents.forEach((event) => {
// Follow list author // Follow list author
if (event.pubkey) { if (event.pubkey) {
@ -113,8 +101,7 @@ export function extractUniquePersons(
} }
}); });
console.log(`[PersonBuilder] Found ${personMap.size} unique persons`); debug("Extracted persons", { personCount: personMap.size });
console.log(`[PersonBuilder] ${followListPubkeys.size} are from follow lists`);
return personMap; return personMap;
} }
@ -171,8 +158,14 @@ export function createPersonAnchorNodes(
const limitedPersons = eligiblePersons.slice(0, limit); const limitedPersons = eligiblePersons.slice(0, limit);
// Create nodes for the limited set // 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 // Create seeded random generator for consistent positioning
const rng = new SeededRandom(createSeed(pubkey)); const rng = new SeededRandom(createSeed(pubkey));
@ -207,6 +200,8 @@ export function createPersonAnchorNodes(
anchorNodes.push(anchorNode); anchorNodes.push(anchorNode);
}); });
debug("Created person anchor nodes", { count: anchorNodes.length, totalEligible: eligiblePersons.length });
return { return {
nodes: anchorNodes, nodes: anchorNodes,
totalCount: eligiblePersons.length totalCount: eligiblePersons.length
@ -226,6 +221,8 @@ export function createPersonLinks(
nodes: NetworkNode[], nodes: NetworkNode[],
personMap: Map<string, PersonConnection> personMap: Map<string, PersonConnection>
): PersonLink[] { ): PersonLink[] {
debug("Creating person links", { anchorCount: personAnchors.length, nodeCount: nodes.length });
const links: PersonLink[] = []; const links: PersonLink[] = [];
const nodeMap = new Map(nodes.map((n) => [n.id, n])); 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; return links;
} }

12
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 type { NetworkNode, NetworkLink, GraphData, GraphState } from "../types";
import { getMatchingTags } from '$lib/utils/nostrUtils'; import { getMatchingTags } from '$lib/utils/nostrUtils';
import { createNetworkNode, createEventMap, extractEventIdFromATag, getEventColor } from './networkBuilder'; import { createNetworkNode, createEventMap, extractEventIdFromATag, getEventColor } from './networkBuilder';
import { createDebugFunction } from './common';
// Configuration // Configuration
const DEBUG = false;
const INDEX_EVENT_KIND = 30040; const INDEX_EVENT_KIND = 30040;
const CONTENT_EVENT_KIND = 30041; const CONTENT_EVENT_KIND = 30041;
/** // Debug function
* Debug logging function const debug = createDebugFunction("StarNetworkBuilder");
*/
function debug(...args: any[]) {
if (DEBUG) {
console.log("[StarNetworkBuilder]", ...args);
}
}
/** /**
* Represents a star network with a central index node and peripheral content nodes * Represents a star network with a central index node and peripheral content nodes

35
src/lib/navigator/EventNetwork/utils/tagNetworkBuilder.ts

@ -8,29 +8,16 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NetworkNode, NetworkLink, GraphData } from "../types"; import type { NetworkNode, NetworkLink, GraphData } from "../types";
import { getDisplayNameSync } from "$lib/utils/profileCache"; import { getDisplayNameSync } from "$lib/utils/profileCache";
import { SeededRandom, createDebugFunction } from "./common";
// Configuration // Configuration
const TAG_ANCHOR_RADIUS = 15; const TAG_ANCHOR_RADIUS = 15;
// TODO: Move this to settings panel for user control // 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 const TAG_ANCHOR_PLACEMENT_RADIUS = 1250; // Radius from center within which to randomly place tag anchors
/** // Debug function
* Simple seeded random number generator (using a Linear Congruential Generator) const debug = createDebugFunction("TagNetworkBuilder");
* This ensures consistent positioning for the same tag values across sessions
*/
class SeededRandom {
private seed: number;
constructor(seed: number) {
this.seed = seed;
}
// 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 * Creates a deterministic seed from a string
@ -76,8 +63,7 @@ export function extractUniqueTagsForType(
): Map<string, Set<string>> { ): Map<string, Set<string>> {
// Map of tagValue -> Set of event IDs // Map of tagValue -> Set of event IDs
const tagMap = new Map<string, Set<string>>(); const tagMap = new Map<string, Set<string>>();
debug("Extracting unique tags for type", { tagType, eventCount: events.length });
console.log(`[TagBuilder] Extracting tags of type: ${tagType} from ${events.length} events`);
events.forEach((event) => { events.forEach((event) => {
if (!event.tags || !event.id) return; 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; return tagMap;
} }
@ -114,6 +100,8 @@ export function createTagAnchorNodes(
): NetworkNode[] { ): NetworkNode[] {
const anchorNodes: NetworkNode[] = []; const anchorNodes: NetworkNode[] = [];
debug("Creating tag anchor nodes", { tagType, tagCount: tagMap.size });
// Calculate positions for tag anchors randomly within radius // Calculate positions for tag anchors randomly within radius
// Show all tags regardless of how many events they appear in // Show all tags regardless of how many events they appear in
const minEventCount = 1; const minEventCount = 1;
@ -173,6 +161,7 @@ export function createTagAnchorNodes(
anchorNodes.push(anchorNode); anchorNodes.push(anchorNode);
}); });
debug("Created tag anchor nodes", { count: anchorNodes.length });
return anchorNodes; return anchorNodes;
} }
@ -183,6 +172,8 @@ export function createTagLinks(
tagAnchors: NetworkNode[], tagAnchors: NetworkNode[],
nodes: NetworkNode[], nodes: NetworkNode[],
): NetworkLink[] { ): NetworkLink[] {
debug("Creating tag links", { anchorCount: tagAnchors.length, nodeCount: nodes.length });
const links: NetworkLink[] = []; const links: NetworkLink[] = [];
const nodeMap = new Map(nodes.map((n) => [n.id, n])); 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; return links;
} }
@ -215,6 +207,8 @@ export function enhanceGraphWithTags(
height: number, height: number,
displayLimit?: number, displayLimit?: number,
): GraphData { ): GraphData {
debug("Enhancing graph with tags", { tagType, displayLimit });
// Extract unique tags for the specified type // Extract unique tags for the specified type
const tagMap = extractUniqueTagsForType(events, tagType); const tagMap = extractUniqueTagsForType(events, tagType);
@ -223,7 +217,6 @@ export function enhanceGraphWithTags(
// Apply display limit if provided // Apply display limit if provided
if (displayLimit && displayLimit > 0 && tagAnchors.length > displayLimit) { 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) // Sort by connection count (already done in createTagAnchorNodes)
// and take only the top ones up to the limit // and take only the top ones up to the limit
tagAnchors = tagAnchors.slice(0, displayLimit); tagAnchors = tagAnchors.slice(0, displayLimit);
@ -266,6 +259,8 @@ export function createTagGravityForce(
} }
}); });
debug("Creating tag gravity force");
// Custom force function // Custom force function
function force(alpha: number) { function force(alpha: number) {
nodes.forEach((node) => { nodes.forEach((node) => {

19
src/lib/stores/displayLimits.ts

@ -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<DisplayLimits>({
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;
}

123
src/lib/stores/visualizationConfig.ts

@ -3,6 +3,7 @@ import { writable, derived, get } from "svelte/store";
export interface EventKindConfig { export interface EventKindConfig {
kind: number; kind: number;
limit: number; limit: number;
enabled?: boolean; // Whether this kind is enabled for display
nestedLevels?: number; // Only for kind 30040 nestedLevels?: number; // Only for kind 30040
depth?: number; // Only for kind 3 (follow lists) depth?: number; // Only for kind 3 (follow lists)
showAll?: boolean; // Only for content kinds (30041, 30818) - show all loaded content instead of limit showAll?: boolean; // Only for content kinds (30041, 30818) - show all loaded content instead of limit
@ -14,50 +15,26 @@ export interface VisualizationConfig {
// Graph traversal // Graph traversal
searchThroughFetched: boolean; 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 // Default configurations for common event kinds
const DEFAULT_EVENT_CONFIGS: EventKindConfig[] = [ const DEFAULT_EVENT_CONFIGS: EventKindConfig[] = [
{ kind: 0, limit: 5 }, // Metadata events (profiles) - controls how many profiles to display { kind: 0, limit: 5, enabled: false }, // 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: 3, limit: 0, depth: 0, enabled: false }, // Follow lists - limit 0 = don't fetch, >0 = fetch follow lists
{ kind: 30040, limit: 20, nestedLevels: 1 }, { kind: 30040, limit: 20, nestedLevels: 1, enabled: true },
{ kind: 30041, limit: 20 }, { kind: 30041, limit: 20, enabled: false },
{ kind: 30818, limit: 20 }, { kind: 30818, limit: 20, enabled: false },
]; ];
function createVisualizationConfig() { function createVisualizationConfig() {
// Initialize with both new and legacy properties
const initialConfig: VisualizationConfig = { const initialConfig: VisualizationConfig = {
eventConfigs: DEFAULT_EVENT_CONFIGS, eventConfigs: DEFAULT_EVENT_CONFIGS,
searchThroughFetched: true, 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 } = const { subscribe, set, update } =
writable<VisualizationConfig>(initialConfig); writable<VisualizationConfig>(initialConfig);
// Helper to sync legacy properties with eventConfigs
const syncLegacyProperties = (config: VisualizationConfig) => {
config.allowedKinds = config.eventConfigs.map((ec) => ec.kind);
return config;
};
return { return {
subscribe, subscribe,
update, update,
@ -71,7 +48,7 @@ function createVisualizationConfig() {
return config; return config;
} }
const newConfig: EventKindConfig = { kind, limit }; const newConfig: EventKindConfig = { kind, limit, enabled: true };
// Add nestedLevels for 30040 // Add nestedLevels for 30040
if (kind === 30040) { if (kind === 30040) {
newConfig.nestedLevels = 1; newConfig.nestedLevels = 1;
@ -85,20 +62,7 @@ function createVisualizationConfig() {
...config, ...config,
eventConfigs: [...config.eventConfigs, newConfig], eventConfigs: [...config.eventConfigs, newConfig],
}; };
return syncLegacyProperties(updated); return 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);
}), }),
// Remove an event kind // Remove an event kind
@ -108,17 +72,7 @@ function createVisualizationConfig() {
...config, ...config,
eventConfigs: config.eventConfigs.filter((ec) => ec.kind !== kind), eventConfigs: config.eventConfigs.filter((ec) => ec.kind !== kind),
}; };
return syncLegacyProperties(updated); return 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);
}), }),
// Update limit for a specific kind // Update limit for a specific kind
@ -172,48 +126,13 @@ function createVisualizationConfig() {
searchThroughFetched: !config.searchThroughFetched, searchThroughFetched: !config.searchThroughFetched,
})), })),
toggleAppendMode: () => // Toggle enabled state for a specific kind
update((config) => ({
...config,
appendMode: !config.appendMode,
})),
// Legacy methods for backward compatibility
toggleKind: (kind: number) => 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) => ({ update((config) => ({
...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 // Helper to get all enabled event kinds
export const enabledEventKinds = derived(visualizationConfig, ($config) => 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 // Helper to check if a kind is enabled
export const isKindEnabled = derived( 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, visualizationConfig,
($config) => (kind: number) => { ($config) => (kind: number) => {
const inEventConfigs = $config.eventConfigs.some((ec) => ec.kind === kind); const eventConfig = $config.eventConfigs.find((ec) => ec.kind === kind);
const notDisabled = !($config.disabledKinds?.includes(kind) || false); return eventConfig ? eventConfig.enabled !== false : false;
return inEventConfigs && notDisabled;
}, },
); );

62
src/lib/utils/displayLimits.ts

@ -1,34 +1,28 @@
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import type { DisplayLimits } from '$lib/stores/displayLimits';
import type { VisualizationConfig } from '$lib/stores/visualizationConfig'; 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 events - All available events
* @param limits - Display limit settings * @param config - Visualization configuration
* @param config - Visualization configuration (optional)
* @returns Filtered events that should be displayed * @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 result: NDKEvent[] = [];
const kindCounts = new Map<number, number>(); const kindCounts = new Map<number, number>();
for (const event of events) { 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; const kind = event.kind;
if (kind === undefined) continue; if (kind === undefined) continue;
// Get the limit for this event kind from the config // Get the config for this event kind
const eventConfig = config?.eventConfigs.find(ec => ec.kind === 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; const limit = eventConfig?.limit;
// Special handling for content kinds (30041, 30818) with showAll option // Special handling for content kinds (30041, 30818) with showAll option
@ -105,37 +99,3 @@ export function detectMissingEvents(events: NDKEvent[], existingIds: Set<string>
return missing; return missing;
} }
/**
* Groups events by kind for easier counting and display
*/
export function groupEventsByKind(events: NDKEvent[]): Map<number, NDKEvent[]> {
const groups = new Map<number, NDKEvent[]>();
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<number, number> {
const counts = new Map<number, number>();
for (const event of events) {
const kind = event.kind;
if (kind !== undefined) {
counts.set(kind, (counts.get(kind) || 0) + 1);
}
}
return counts;
}

10
src/lib/utils/eventColors.ts

@ -80,13 +80,3 @@ export function getEventKindName(kind: number): string {
return kindNames[kind] || `Kind ${kind}`; 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();
}

108
src/routes/visualize/+page.svelte

@ -12,7 +12,6 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { filterValidIndexEvents } from "$lib/utils"; import { filterValidIndexEvents } from "$lib/utils";
import { networkFetchLimit } from "$lib/state"; import { networkFetchLimit } from "$lib/state";
import { displayLimits } from "$lib/stores/displayLimits";
import { visualizationConfig, type EventKindConfig } from "$lib/stores/visualizationConfig"; import { visualizationConfig, type EventKindConfig } from "$lib/stores/visualizationConfig";
import { filterByDisplayLimits, detectMissingEvents } from "$lib/utils/displayLimits"; import { filterByDisplayLimits, detectMissingEvents } from "$lib/utils/displayLimits";
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -43,7 +42,6 @@
let loading = $state(true); let loading = $state(true);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let showSettings = $state(false); let showSettings = $state(false);
let tagExpansionDepth = $state(0);
let baseEvents = $state<NDKEvent[]>([]); // Store original events before expansion let baseEvents = $state<NDKEvent[]>([]); // Store original events before expansion
let missingEventIds = $state(new Set<string>()); // Track missing referenced events let missingEventIds = $state(new Set<string>()); // Track missing referenced events
let loadingEventKinds = $state<Array<{kind: number, limit: number}>>([]); // Track what kinds are being loaded let loadingEventKinds = $state<Array<{kind: number, limit: number}>>([]); // Track what kinds are being loaded
@ -486,27 +484,9 @@
finalEventMap.set(event.id, event); finalEventMap.set(event.id, event);
}); });
// Handle append mode // Replace mode (always replace, no append mode)
if ($visualizationConfig.appendMode && allEvents.length > 0) { allEvents = Array.from(finalEventMap.values());
// Merge existing events with new events followListEvents = [];
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 = [];
}
}
baseEvents = [...allEvents]; // Store base events for tag expansion baseEvents = [...allEvents]; // Store base events for tag expansion
@ -571,7 +551,7 @@
} }
// Step 7: Apply display limits // Step 7: Apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); events = filterByDisplayLimits(allEvents, $visualizationConfig);
// Step 8: Detect missing events // Step 8: Detect missing events
const eventIds = new Set(allEvents.map(e => e.id)); const eventIds = new Set(allEvents.map(e => e.id));
@ -580,7 +560,6 @@
debug("Total events fetched:", allEvents.length); debug("Total events fetched:", allEvents.length);
debug("Events displayed:", events.length); debug("Events displayed:", events.length);
debug("Missing event IDs:", missingEventIds.size); debug("Missing event IDs:", missingEventIds.size);
debug("Display limits:", $displayLimits);
debug("About to set loading to false"); debug("About to set loading to false");
debug("Current loading state:", loading); debug("Current loading state:", loading);
} catch (e) { } catch (e) {
@ -604,7 +583,7 @@
if (depth === 0 || tags.length === 0) { if (depth === 0 || tags.length === 0) {
// Reset to base events only // Reset to base events only
allEvents = [...baseEvents]; allEvents = [...baseEvents];
events = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); events = filterByDisplayLimits(allEvents, $visualizationConfig);
return; return;
} }
@ -789,7 +768,7 @@
} }
// Apply display limits // Apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits); events = filterByDisplayLimits(allEvents, $visualizationConfig);
// Update missing events detection // Update missing events detection
const eventIds = new Set(allEvents.map(e => e.id)); 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 // React to display limit and allowed kinds changes
$effect(() => { $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) { if (allEvents.length > 0) {
const newEvents = filterByDisplayLimits(allEvents, $displayLimits, $visualizationConfig); const newEvents = filterByDisplayLimits(allEvents, $visualizationConfig);
// Only update if actually different to avoid infinite loops // Only update if actually different to avoid infinite loops
if (newEvents.length !== events.length) { if (newEvents.length !== events.length) {
@ -893,12 +808,6 @@
debug("Effect: events filtered to", events.length, "missing:", missingEventIds.size); 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} onupdate={fetchEvents}
onclear={clearEvents} onclear={clearEvents}
onTagExpansionChange={handleTagExpansion} onTagExpansionChange={handleTagExpansion}
onFetchMissing={fetchMissingEvents}
{profileStats} {profileStats}
{allEventCounts} {allEventCounts}
/> />

279
tests/e2e/collapsible-sections.pw.spec.ts

@ -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();
});
});
});

18
tests/e2e/example.pw.spec.ts

@ -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();
});

365
tests/e2e/poc-performance-validation.pw.spec.ts

@ -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);
});
});

308
tests/e2e/tag-anchor-interactions.pw.spec.ts

@ -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();
}
});
});

150
tests/e2e/test-results/poc-performance-validation-061e6-ation-during-visual-updates-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-20b81-ges-are-handled-efficiently-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-22ad4-mulation-maintains-momentum-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-2b829-gle-uses-visual-update-path-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-89786--vs-full-update-performance-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-8f95e-gle-uses-visual-update-path-chromium/error-context.md

@ -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`);
```

150
tests/e2e/test-results/poc-performance-validation-c97c0-ility-during-visual-updates-chromium/error-context.md

@ -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`);
```

382
tests/integration/displayLimitsIntegration.test.ts

@ -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<number, number>();
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<string, NDKEvent[]>();
// 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<number, number>();
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);
});
});
});

99
tests/integration/markupIntegration.test.ts

@ -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 <h1> tags)
expect(output).toContain('This is a test');
expect(output).toContain('============');
expect(output).toContain('### Disclaimer');
// Unordered list
expect(output).toContain('<ul');
expect(output).toContain('but');
// Ordered list
expect(output).toContain('<ol');
expect(output).toContain('first');
// Nested lists
expect(output).toMatch(/<ul[^>]*>.*<ul[^>]*>/s);
// Blockquotes
expect(output).toContain('<blockquote');
expect(output).toContain('This is important information');
// Inline code
expect(output).toContain('<div class="leather min-h-full w-full flex flex-col items-center">');
// Images
expect(output).toMatch(/<img[^>]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/);
// Links
expect(output).toMatch(/<a[^>]+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(/<iframe[^>]+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('<h1');
expect(output).toContain('<h2');
expect(output).toContain('Disclaimer');
// Unordered list
expect(output).toContain('<ul');
expect(output).toContain('but');
// Ordered list
expect(output).toContain('<ol');
expect(output).toContain('first');
// Nested lists
expect(output).toMatch(/<ul[^>]*>.*<ul[^>]*>/s);
// Blockquotes
expect(output).toContain('<blockquote');
expect(output).toContain('This is important information');
// Inline code
expect(output).toMatch(/<code[^>]*>.*leather min-h-full w-full flex flex-col items-center.*<\/code>/s);
// Images
expect(output).toMatch(/<img[^>]+src="https:\/\/upload\.wikimedia\.org\/wikipedia\/commons\/f\/f1\/Heart_coraz%C3%B3n\.svg"/);
// Links
expect(output).toMatch(/<a[^>]+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(/<iframe[^>]+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('<hr');
// Footnote references and section
expect(output).toContain('Footnotes');
expect(output).toMatch(/<li id=\"fn-1\">/);
// Table
expect(output).toContain('<table');
// Code blocks
expect(output).toContain('<pre');
});
});

244
tests/integration/markupTestfile.md

@ -1,244 +0,0 @@
This is a test
============
### Disclaimer
It is _only_ a test, for __sure__. I just wanted to see if the markup renders correctly on the page, even if I use **two asterisks** for bold text, instead of *one asterisk*.[^1]
# H1
## H2
### H3
#### H4
##### H5
###### H6
This file is full of ~errors~ opportunities to ~~mess up the formatting~~ check your markup parser.
You can even learn about [[mirepoix]], [[nkbip-03]], or [[roman catholic church|catholics]]
npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z wrote this. That's the same person as this one with a nostr prefix nostr:npub1l5sga6xg72phsz5422ykujprejwud075ggrr3z2hwyrfgr7eylqstegx9z and nprofile1qydhwumn8ghj7argv4nx7un9wd6zumn0wd68yvfwvdhk6tcpr3mhxue69uhkx6rjd9ehgurfd3kzumn0wd68yvfwvdhk6tcqyr7jprhgeregx7q2j4fgjmjgy0xfm34l63pqvwyf2acsd9q0mynuzp4qva3. That is a different person from npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz.
> 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 `<div class="leather min-h-full w-full flex flex-col items-center">` 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<<i)) != 0);
}
```
Asciidoc:
```adoc
= Header 1
preamble goes here
== Header 2
some more text
```
Gherkin:
```gherkin
Feature: Account Holder withdraws cash
Scenario: Account has sufficient funds
Given The account balance is $100
And the card is valid
And the machine contains enough money
When the Account Holder requests $20
Then the ATM should dispense $20
And the account balance should be $80
And the card should be returned
```
Go:
```go
package main
import (
"fmt"
"bufio"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
fmt.Print("Enter text: ")
scanner.Scan()
input := scanner.Text()
fmt.Println("You entered:", input)
}
```
or even markup:
```md
A H1 Header
============
Paragraphs are separated by a blank line.
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
look like:
* this one[^some reference text]
* that one
* the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> 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

118
tests/unit/advancedMarkupParser.test.ts

@ -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('<strong>bold</strong>');
expect(output).toContain('<em>italic</em>');
expect(output).toContain('<del class="line-through">strikethrough</del>');
});
it('parses blockquotes', async () => {
const input = '> quote';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<blockquote');
expect(output).toContain('quote');
});
it('parses multi-line blockquotes', async () => {
const input = '> quote\n> quote';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<blockquote');
expect(output).toContain('quote');
expect(output).toContain('quote');
});
it('parses unordered lists', async () => {
const input = '* a\n* b';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<ul');
expect(output).toContain('a');
expect(output).toContain('b');
});
it('parses ordered lists', async () => {
const input = '1. one\n2. two';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<ol');
expect(output).toContain('one');
expect(output).toContain('two');
});
it('parses links and images', async () => {
const input = '[link](https://example.com) ![alt](https://img.com/x.png)';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<a');
expect(output).toContain('<img');
});
it('parses hashtags', async () => {
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('<table');
expect(output).toContain('Header');
expect(output).toContain('a');
});
it('parses code blocks (with and without language)', async () => {
const input = '```js\nconsole.log(1);\n```\n```\nno lang\n```';
const output = await parseAdvancedmarkup(input);
const textOnly = output.replace(/<[^>]+>/g, '');
expect(output).toContain('<pre');
expect(textOnly).toContain('console.log(1);');
expect(textOnly).toContain('no lang');
});
it('parses horizontal rules', async () => {
const input = '---';
const output = await parseAdvancedmarkup(input);
expect(output).toContain('<hr');
});
it('parses footnotes (references and section)', async () => {
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');
});
});

88
tests/unit/basicMarkupParser.test.ts

@ -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('<strong>bold</strong>');
expect(output).toContain('<em>italic</em>');
expect(output).toContain('<del class="line-through">strikethrough</del>');
});
it('parses blockquotes', async () => {
const input = '> quote';
const output = await parseBasicmarkup(input);
expect(output).toContain('<blockquote');
expect(output).toContain('quote');
});
it('parses multi-line blockquotes', async () => {
const input = '> quote\n> quote';
const output = await parseBasicmarkup(input);
expect(output).toContain('<blockquote');
expect(output).toContain('quote');
expect(output).toContain('quote');
});
it('parses unordered lists', async () => {
const input = '* a\n* b';
const output = await parseBasicmarkup(input);
expect(output).toContain('<ul');
expect(output).toContain('a');
expect(output).toContain('b');
});
it('parses ordered lists', async () => {
const input = '1. one\n2. two';
const output = await parseBasicmarkup(input);
expect(output).toContain('<ol');
expect(output).toContain('one');
expect(output).toContain('two');
});
it('parses links and images', async () => {
const input = '[link](https://example.com) ![alt](https://img.com/x.png)';
const output = await parseBasicmarkup(input);
expect(output).toContain('<a');
expect(output).toContain('<img');
});
it('parses hashtags', async () => {
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');
});
});

376
tests/unit/coordinateDeduplication.test.ts

@ -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<NDKEvent> {
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);
});
});
});

143
tests/unit/linkRenderingDebug.test.ts

@ -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);
}
});
});
});
});

436
tests/unit/visualizationReactivity.extended.test.ts

@ -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<string>();
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<string, { x: number; y: number }>();
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);
});
});
});
Loading…
Cancel
Save