39 changed files with 158 additions and 5047 deletions
@ -1,105 +0,0 @@
@@ -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 |
||||
@ -1,332 +0,0 @@
@@ -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* |
||||
@ -1,124 +0,0 @@
@@ -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* |
||||
@ -1,168 +0,0 @@
@@ -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* |
||||
@ -0,0 +1,41 @@
@@ -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); |
||||
} |
||||
}; |
||||
} |
||||
@ -1,19 +0,0 @@
@@ -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; |
||||
} |
||||
@ -1,279 +0,0 @@
@@ -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(); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,18 +0,0 @@
@@ -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(); |
||||
}); |
||||
@ -1,365 +0,0 @@
@@ -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); |
||||
}); |
||||
}); |
||||
@ -1,308 +0,0 @@
@@ -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(); |
||||
} |
||||
}); |
||||
}); |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,150 +0,0 @@
@@ -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`); |
||||
``` |
||||
@ -1,382 +0,0 @@
@@ -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); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,99 +0,0 @@
@@ -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'); |
||||
}); |
||||
});
|
||||
@ -1,244 +0,0 @@
@@ -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\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] |
||||
|
||||
 |
||||
|
||||
### 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 |
||||
@ -1,118 +0,0 @@
@@ -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) '; |
||||
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'); |
||||
}); |
||||
});
|
||||
@ -1,88 +0,0 @@
@@ -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) '; |
||||
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'); |
||||
}); |
||||
});
|
||||
@ -1,376 +0,0 @@
@@ -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); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,143 +0,0 @@
@@ -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); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,436 +0,0 @@
@@ -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…
Reference in new issue