615 lines
19 KiB
TypeScript
615 lines
19 KiB
TypeScript
/**
|
|
* Analyzes the captured network traffic to document the session join flow
|
|
* and create a migration test plan
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
interface ApiCall {
|
|
step: string;
|
|
method?: string;
|
|
url: string;
|
|
status?: number;
|
|
timestamp: string;
|
|
}
|
|
|
|
interface WebSocketMessage {
|
|
event: string;
|
|
payload?: string;
|
|
url?: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
function analyzeApiCalls(apiCalls: ApiCall[]): Map<string, ApiCall[]> {
|
|
const apiMap = new Map<string, ApiCall[]>();
|
|
|
|
for (const call of apiCalls) {
|
|
if (call.step === 'REQUEST' && call.url.includes('/api/')) {
|
|
// Extract the API endpoint path
|
|
const url = new URL(call.url);
|
|
const endpoint = `${call.method} ${url.pathname}`;
|
|
|
|
if (!apiMap.has(endpoint)) {
|
|
apiMap.set(endpoint, []);
|
|
}
|
|
apiMap.get(endpoint)!.push(call);
|
|
}
|
|
}
|
|
|
|
return apiMap;
|
|
}
|
|
|
|
function analyzeWebSocketMessages(wsMessages: WebSocketMessage[]): Map<string, number> {
|
|
const messageTypes = new Map<string, number>();
|
|
|
|
for (const msg of wsMessages) {
|
|
if (msg.event === 'sent' || msg.event === 'received') {
|
|
try {
|
|
const payload = JSON.parse(msg.payload || '{}');
|
|
|
|
// Extract message type or method name
|
|
let messageType = 'unknown';
|
|
|
|
if (payload.type) {
|
|
messageType = `TYPE:${payload.type}`;
|
|
} else if (payload.method) {
|
|
messageType = `METHOD:${payload.method}`;
|
|
} else if (payload.args && payload.args.length > 0) {
|
|
// Try to parse the args to get more info
|
|
try {
|
|
const argsData = JSON.parse(payload.args[0]);
|
|
if (argsData.method_name) {
|
|
messageType = `NATIVE_METHOD:${argsData.method_name}`;
|
|
} else if (argsData.event_id) {
|
|
messageType = `NATIVE_EVENT:${argsData.event_id}`;
|
|
}
|
|
} catch (e) {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
|
|
messageTypes.set(messageType, (messageTypes.get(messageType) || 0) + 1);
|
|
} catch (e) {
|
|
// Ignore JSON parse errors
|
|
}
|
|
}
|
|
}
|
|
|
|
return messageTypes;
|
|
}
|
|
|
|
function extractSequentialFlow(apiCalls: ApiCall[], wsMessages: WebSocketMessage[]): any[] {
|
|
// Combine and sort by timestamp
|
|
const allEvents = [
|
|
...apiCalls.map(call => ({
|
|
type: call.step,
|
|
subtype: 'API',
|
|
method: call.method,
|
|
url: call.url,
|
|
status: call.status,
|
|
timestamp: new Date(call.timestamp).getTime(),
|
|
})),
|
|
...wsMessages.filter(msg => msg.event === 'sent' || msg.event === 'received').map(msg => ({
|
|
type: msg.event === 'sent' ? 'WS_SENT' : 'WS_RECEIVED',
|
|
subtype: 'WEBSOCKET',
|
|
payload: msg.payload?.substring(0, 200),
|
|
timestamp: new Date(msg.timestamp).getTime(),
|
|
})),
|
|
].sort((a, b) => a.timestamp - b.timestamp);
|
|
|
|
return allEvents;
|
|
}
|
|
|
|
function main() {
|
|
const testResultsDir = path.join(__dirname, '../test-results');
|
|
|
|
console.log('='.repeat(80));
|
|
console.log('SESSION JOIN FLOW ANALYSIS');
|
|
console.log('='.repeat(80));
|
|
console.log();
|
|
|
|
// Read API calls
|
|
const apiCallsPath = path.join(testResultsDir, 'api-calls.json');
|
|
const apiCalls: ApiCall[] = JSON.parse(fs.readFileSync(apiCallsPath, 'utf-8'));
|
|
console.log(`✓ Loaded ${apiCalls.length} API call records`);
|
|
|
|
// Read WebSocket messages
|
|
const wsMessagesPath = path.join(testResultsDir, 'websocket-messages.json');
|
|
const wsMessages: WebSocketMessage[] = JSON.parse(fs.readFileSync(wsMessagesPath, 'utf-8'));
|
|
console.log(`✓ Loaded ${wsMessages.length} WebSocket message records`);
|
|
console.log();
|
|
|
|
// Analyze API calls
|
|
console.log('API ENDPOINTS CALLED');
|
|
console.log('-'.repeat(80));
|
|
const apiMap = analyzeApiCalls(apiCalls);
|
|
const sortedApis = Array.from(apiMap.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
|
|
for (const [endpoint, calls] of sortedApis) {
|
|
console.log(` ${endpoint.padEnd(60)} (${calls.length} calls)`);
|
|
}
|
|
console.log();
|
|
|
|
// Analyze WebSocket messages
|
|
console.log('WEBSOCKET MESSAGE TYPES');
|
|
console.log('-'.repeat(80));
|
|
const messageTypes = analyzeWebSocketMessages(wsMessages);
|
|
const sortedTypes = Array.from(messageTypes.entries()).sort((a, b) => b[1] - a[1]);
|
|
|
|
for (const [type, count] of sortedTypes.slice(0, 20)) {
|
|
console.log(` ${type.padEnd(60)} (${count} times)`);
|
|
}
|
|
console.log(` ... and ${sortedTypes.length - 20} more message types`);
|
|
console.log();
|
|
|
|
// Generate migration plan
|
|
const planPath = path.join(testResultsDir, 'SESSION_MIGRATION_PLAN.md');
|
|
|
|
const plan = `# Session Join Flow Migration Test Plan
|
|
|
|
## Overview
|
|
|
|
This document outlines the complete test plan for migrating the "Joining a Music Session" feature from the legacy JamKazam application to the new React-based jam-ui application.
|
|
|
|
**Captured on:** ${new Date().toISOString()}
|
|
**Legacy URL:** http://www.jamkazam.local:3100
|
|
**Target:** jam-ui React application
|
|
|
|
## Overview of Flow
|
|
|
|
The session join flow consists of 5 main steps:
|
|
1. User Authentication (login)
|
|
2. Dashboard Load (with WebSocket connection to native client)
|
|
3. Skip Upgrade Modal (keyboard shortcut)
|
|
4. Navigate to Create Session
|
|
5. Create Quick Start Session (joins the session)
|
|
|
|
Note: The Ctrl+Shift+0 keyboard shortcut for enabling native client features is handled during session creation (Step 5), not as a separate step.
|
|
|
|
## Session Join Flow Steps
|
|
|
|
### Step 1: User Authentication
|
|
**Description:** User logs into the application
|
|
|
|
**API Calls Required:**
|
|
- POST /api/sessions (login endpoint)
|
|
- GET /api/users/{user_id} (fetch user profile)
|
|
- GET /api/genres (load genre data)
|
|
- GET /api/countries (load country data)
|
|
|
|
**Test Requirements:**
|
|
- Verify authentication token is stored in cookies
|
|
- Verify user session is established
|
|
- Verify redirect to dashboard after successful login
|
|
|
|
---
|
|
|
|
### Step 2: Dashboard Load
|
|
**Description:** Load user dashboard with all necessary data
|
|
|
|
**API Calls Required:**
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) => !endpoint.includes('paypal') && !endpoint.includes('session_create'))
|
|
.slice(0, 20)
|
|
.map(([endpoint]) => `- ${endpoint}`)
|
|
.join('\n')}
|
|
|
|
**WebSocket Connection:**
|
|
- Establish WebSocket connection to native client (ws://localhost:3060/)
|
|
- Initialize jkfrontendchannel communication
|
|
- Exchange handshake messages
|
|
|
|
**Native Client Messages:**
|
|
${Array.from(messageTypes.entries())
|
|
.filter(([type]) => type.startsWith('NATIVE_METHOD:'))
|
|
.slice(0, 10)
|
|
.map(([type, count]) => `- ${type} (${count} times)`)
|
|
.join('\n')}
|
|
|
|
**Test Requirements:**
|
|
- Verify all dashboard data loads correctly
|
|
- Verify WebSocket connection establishes successfully
|
|
- Verify native client communication is functional
|
|
- Verify user can see friends, notifications, and session options
|
|
|
|
---
|
|
|
|
### Step 3: Skip Upgrade Modal
|
|
**Description:** Handle upgrade modal on dashboard
|
|
|
|
**User Action:** Press Cmd+Shift+0 (Mac) or Ctrl+Shift+0 (Windows)
|
|
|
|
**API Calls Made:**
|
|
- GET /api/versioncheck
|
|
|
|
**WebSocket Connection Established:**
|
|
- Opens WebSocket connection to server: `ws://www.jamkazam.local:6767/websocket?channel_id=...&client_type=browser...`
|
|
- This is a CRITICAL connection - not just a UI interaction
|
|
- Approximately 100 WebSocket messages exchanged during initialization
|
|
|
|
**Expected Behavior:**
|
|
- Upgrade modal dismisses
|
|
- Server WebSocket connection established
|
|
- User can interact with dashboard
|
|
|
|
**Test Requirements:**
|
|
- Verify keyboard shortcut works
|
|
- Verify modal closes without errors
|
|
- **Verify WebSocket connection to server is established**
|
|
- Verify dashboard remains functional
|
|
|
|
---
|
|
|
|
### Step 4: Navigate to Create Session
|
|
**Description:** User clicks "Create Session" tile
|
|
|
|
**API Calls Made:**
|
|
- GET /api/sessions/scheduled (fetch any scheduled sessions)
|
|
- GET /api/users/{user_id} (refresh user profile)
|
|
- GET /api/jamtracks/purchased (refresh purchased tracks)
|
|
|
|
**Expected Behavior:**
|
|
- Navigate to session creation page
|
|
- Load session creation UI
|
|
- Maintain existing WebSocket connection from Step 3
|
|
- Load list of scheduled sessions
|
|
|
|
**Test Requirements:**
|
|
- Verify navigation occurs
|
|
- Verify session creation options are visible
|
|
- Verify scheduled sessions are loaded
|
|
- Verify no API errors occur
|
|
- Verify WebSocket connection remains active
|
|
|
|
---
|
|
|
|
### Step 5: Create Quick Start Session
|
|
**Description:** User creates a quick start session
|
|
|
|
**User Action:** Press Ctrl+Shift+0 to enable native client features, then click "Create Quick Start" button
|
|
|
|
**API Calls Made (28 total):**
|
|
|
|
**Critical New API Call:**
|
|
- **POST /api/users/{user_id}/udp_reachable** - Reports UDP reachability for P2P connections
|
|
|
|
**Dashboard Data Reload (triggered by Ctrl+Shift+0):**
|
|
- GET /api/users/{user_id} (2 times)
|
|
- GET /api/countries
|
|
- GET /api/shopping_carts
|
|
- GET /api/genres (3 times)
|
|
- GET /api/instruments (3 times)
|
|
- GET /api/users/{user_id}/friends (2 times)
|
|
- GET /api/jamtracks/purchased (3 times)
|
|
- GET /api/languages
|
|
- GET /api/subjects
|
|
- GET /api/chat
|
|
- GET /api/sessions/scheduled
|
|
- GET /api/users/{user_id}/notifications
|
|
- GET /api/regions
|
|
- GET /api/versioncheck (3 times)
|
|
- GET /api/teacher_distributions
|
|
- GET /api/config/client
|
|
- GET /api/users/{user_id}/broadcast_notification
|
|
|
|
**Expected Session Creation API Calls:**
|
|
- POST /api/music_sessions (create session)
|
|
- GET /api/music_sessions/{session_id} (fetch session details)
|
|
- GET /api/music_sessions/{session_id}/participants (fetch participants)
|
|
|
|
**WebSocket Connections Established:**
|
|
|
|
1. **Native Client Connection:**
|
|
- URL: `ws://localhost:3060/`
|
|
- Purpose: Audio/video streaming and control
|
|
- ~100+ messages during initialization
|
|
|
|
2. **Server Connection:**
|
|
- URL: `ws://www.jamkazam.local:6767/websocket?channel_id=...&client_id=...`
|
|
- Purpose: Session coordination and real-time updates
|
|
- ~100+ messages during initialization
|
|
|
|
**Key Native Client Operations:**
|
|
- NetworkTestResult calls
|
|
- getConnectionDetail calls
|
|
- SetLatencyTestBlocked
|
|
- SetScoreWorkTimingInterval
|
|
|
|
**Important Notes:**
|
|
- Pressing Ctrl+Shift+0 essentially "reinitializes" the client
|
|
- It triggers a full dashboard data reload
|
|
- It establishes BOTH WebSocket connections simultaneously
|
|
- UDP reachability check is performed before session creation
|
|
|
|
**Test Requirements:**
|
|
- Verify Ctrl+Shift+0 triggers dashboard data reload
|
|
- Verify POST /api/users/{user_id}/udp_reachable is called
|
|
- Verify native client WebSocket connection (ws://localhost:3060/)
|
|
- Verify server WebSocket connection is established
|
|
- Verify session is created successfully
|
|
- Verify session ID is returned
|
|
- Verify WebSocket messages are exchanged correctly
|
|
- Verify user enters session interface
|
|
- Verify audio setup begins
|
|
|
|
---
|
|
|
|
## Complete API Endpoint Reference
|
|
|
|
### Authentication & User Management
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) => endpoint.includes('/users') || endpoint.includes('/session'))
|
|
.map(([endpoint, calls]) => `- ${endpoint} (called ${calls.length} times)`)
|
|
.join('\n')}
|
|
|
|
### Static Data Loading
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) =>
|
|
endpoint.includes('/genres') ||
|
|
endpoint.includes('/instruments') ||
|
|
endpoint.includes('/countries') ||
|
|
endpoint.includes('/languages') ||
|
|
endpoint.includes('/regions')
|
|
)
|
|
.map(([endpoint, calls]) => `- ${endpoint} (called ${calls.length} times)`)
|
|
.join('\n')}
|
|
|
|
### Session Management
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) => endpoint.includes('/music_session'))
|
|
.map(([endpoint, calls]) => `- ${endpoint} (called ${calls.length} times)`)
|
|
.join('\n')}
|
|
|
|
### Real-time Features
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) =>
|
|
endpoint.includes('/chat') ||
|
|
endpoint.includes('/notification') ||
|
|
endpoint.includes('/broadcast')
|
|
)
|
|
.map(([endpoint, calls]) => `- ${endpoint} (called ${calls.length} times)`)
|
|
.join('\n')}
|
|
|
|
### System Health
|
|
${Array.from(apiMap.entries())
|
|
.filter(([endpoint]) =>
|
|
endpoint.includes('/healthcheck') ||
|
|
endpoint.includes('/versioncheck') ||
|
|
endpoint.includes('/config')
|
|
)
|
|
.map(([endpoint, calls]) => `- ${endpoint} (called ${calls.length} times)`)
|
|
.join('\n')}
|
|
|
|
---
|
|
|
|
## WebSocket Message Types Reference
|
|
|
|
### Connection & Handshake
|
|
${Array.from(messageTypes.entries())
|
|
.filter(([type]) => type.includes('TYPE:'))
|
|
.map(([type, count]) => `- ${type} (${count} messages)`)
|
|
.join('\n')}
|
|
|
|
### Native Client Methods (Most Frequent)
|
|
${Array.from(messageTypes.entries())
|
|
.filter(([type]) => type.startsWith('NATIVE_METHOD:'))
|
|
.slice(0, 15)
|
|
.map(([type, count]) => `- ${type} (${count} calls)`)
|
|
.join('\n')}
|
|
|
|
### Native Client Events
|
|
${Array.from(messageTypes.entries())
|
|
.filter(([type]) => type.startsWith('NATIVE_EVENT:'))
|
|
.slice(0, 10)
|
|
.map(([type, count]) => `- ${type} (${count} events)`)
|
|
.join('\n')}
|
|
|
|
---
|
|
|
|
## Playwright Test Structure
|
|
|
|
### Test 1: Complete Session Join Flow
|
|
\`\`\`typescript
|
|
test('User can join a music session - complete flow', async ({ page }) => {
|
|
// Step 1: Login
|
|
await loginUser(page, 'user@example.com', 'password');
|
|
|
|
// Step 2: Wait for dashboard to load
|
|
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
|
|
|
|
// Step 3: Skip upgrade modal
|
|
await page.keyboard.press('Meta+Shift+Digit0');
|
|
|
|
// Step 4: Click create session
|
|
await page.click('[data-testid="create-session"]');
|
|
|
|
// Step 5: Create quick start session (Ctrl+Shift+0 handled internally)
|
|
await page.keyboard.press('Control+Shift+Digit0');
|
|
await page.click('[data-testid="quick-start"]');
|
|
|
|
// Verify we're in session
|
|
await expect(page.locator('[data-testid="session-interface"]')).toBeVisible();
|
|
});
|
|
\`\`\`
|
|
|
|
### Test 2: API Calls Validation
|
|
\`\`\`typescript
|
|
test('Session join makes correct API calls', async ({ page }) => {
|
|
const apiCalls: string[] = [];
|
|
|
|
page.on('request', request => {
|
|
if (request.url().includes('/api/')) {
|
|
apiCalls.push(\`\${request.method()} \${new URL(request.url()).pathname}\`);
|
|
}
|
|
});
|
|
|
|
await joinSession(page);
|
|
|
|
// Verify critical API calls were made
|
|
expect(apiCalls).toContain('GET /api/users/');
|
|
expect(apiCalls).toContain('GET /api/genres');
|
|
expect(apiCalls).toContain('POST /api/music_sessions');
|
|
});
|
|
\`\`\`
|
|
|
|
### Test 3: WebSocket Communication
|
|
\`\`\`typescript
|
|
test('Session join establishes WebSocket connection', async ({ page }) => {
|
|
let wsConnected = false;
|
|
let messagesReceived = 0;
|
|
|
|
page.on('websocket', ws => {
|
|
wsConnected = true;
|
|
ws.on('framereceived', () => messagesReceived++);
|
|
});
|
|
|
|
await joinSession(page);
|
|
|
|
expect(wsConnected).toBe(true);
|
|
expect(messagesReceived).toBeGreaterThan(0);
|
|
});
|
|
\`\`\`
|
|
|
|
### Test 4: Native Client Integration
|
|
\`\`\`typescript
|
|
test('Native client methods are called correctly', async ({ page }) => {
|
|
const nativeMethods: string[] = [];
|
|
|
|
page.on('websocket', ws => {
|
|
ws.on('framesent', frame => {
|
|
try {
|
|
const data = JSON.parse(frame.payload.toString());
|
|
if (data.args) {
|
|
const argsData = JSON.parse(data.args[0]);
|
|
if (argsData.method_name) {
|
|
nativeMethods.push(argsData.method_name);
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
});
|
|
|
|
await joinSession(page);
|
|
|
|
// Verify critical native methods were called
|
|
expect(nativeMethods).toContain('getConnectionDetail');
|
|
expect(nativeMethods).toContain('NetworkTestResult');
|
|
});
|
|
\`\`\`
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Phase 1: Authentication & Dashboard
|
|
- [ ] Implement login flow matching legacy API calls
|
|
- [ ] Load all dashboard data (users, genres, instruments, etc.)
|
|
- [ ] Establish WebSocket connection to native client
|
|
- [ ] Implement upgrade modal with keyboard shortcut (Cmd/Ctrl+Shift+0)
|
|
- [ ] Implement dashboard UI with session creation option
|
|
- [ ] Test: User can log in, dismiss modal, and see dashboard
|
|
|
|
### Phase 2: Session Creation UI
|
|
- [ ] Implement "Create Session" navigation
|
|
- [ ] Implement session creation modal/page
|
|
- [ ] Implement keyboard shortcut (Ctrl+Shift+0) for enabling native features
|
|
- [ ] Implement "Quick Start" option
|
|
- [ ] Test: User can navigate to session creation and see options
|
|
|
|
### Phase 3: Native Client Integration
|
|
- [ ] Establish WebSocket communication protocol
|
|
- [ ] Implement native client message handlers
|
|
- [ ] Implement handshake and initialization sequence
|
|
- [ ] Test: WebSocket messages match legacy exactly
|
|
|
|
### Phase 4: Session Creation & Join
|
|
- [ ] Implement session creation API calls
|
|
- [ ] Handle session initialization responses
|
|
- [ ] Implement session interface loading
|
|
- [ ] Coordinate WebSocket messages with session creation
|
|
- [ ] Test: Complete flow works end-to-end
|
|
|
|
### Phase 5: Audio/Video Setup
|
|
- [ ] Implement audio device initialization via native client
|
|
- [ ] Implement latency testing (NetworkTestResult)
|
|
- [ ] Implement connection detail fetching (getConnectionDetail)
|
|
- [ ] Handle real-time audio/video stream setup
|
|
- [ ] Test: Audio setup completes successfully
|
|
|
|
---
|
|
|
|
## Critical Success Criteria
|
|
|
|
1. **API Compatibility**: All API calls must match the sequence and format of the legacy application
|
|
2. **WebSocket Protocol**: WebSocket messages must be identical in structure and timing
|
|
3. **Native Client Communication**: Native client method calls must match exactly
|
|
4. **User Experience**: Flow must be seamless with no additional steps
|
|
5. **Error Handling**: Must handle all error cases gracefully
|
|
6. **Performance**: Page loads and transitions must be as fast or faster than legacy
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests
|
|
- Test individual API call functions
|
|
- Test WebSocket message handlers
|
|
- Test UI component rendering
|
|
|
|
### Integration Tests
|
|
- Test complete authentication flow
|
|
- Test dashboard data loading
|
|
- Test session creation flow
|
|
|
|
### End-to-End Tests (Playwright)
|
|
- Test complete user journey from login to session join
|
|
- Test with real WebSocket connections
|
|
- Test with native client simulation
|
|
|
|
### Comparison Tests
|
|
- Run legacy and new app side-by-side
|
|
- Compare API call sequences
|
|
- Compare WebSocket message sequences
|
|
- Compare timing and performance
|
|
|
|
---
|
|
|
|
## Files Generated
|
|
|
|
- \`test-results/session-join-flow.har\` - Complete HAR file with all network traffic
|
|
- \`test-results/api-calls.json\` - All API calls made during the flow
|
|
- \`test-results/websocket-messages.json\` - All WebSocket messages exchanged
|
|
- \`test-results/all-network-activity.json\` - Combined network activity log
|
|
- \`test-results/*.png\` - Screenshots at various stages
|
|
|
|
---
|
|
|
|
## Next Steps
|
|
|
|
1. Review this plan with the team
|
|
2. Prioritize which components to implement first
|
|
3. Set up Playwright test infrastructure
|
|
4. Begin implementing Phase 1 (Authentication & Dashboard)
|
|
5. Write corresponding Playwright tests for each phase
|
|
6. Run comparison tests against legacy application
|
|
7. Iterate until behavior matches exactly
|
|
|
|
---
|
|
|
|
**Generated by:** Session Flow Analyzer
|
|
**Date:** ${new Date().toLocaleString()}
|
|
**Total API Calls Captured:** ${apiCalls.length}
|
|
**Total WebSocket Messages Captured:** ${wsMessages.length}
|
|
**Unique API Endpoints:** ${apiMap.size}
|
|
**Unique WebSocket Message Types:** ${messageTypes.size}
|
|
`;
|
|
|
|
fs.writeFileSync(planPath, plan);
|
|
console.log(`✅ Migration plan written to: ${planPath}`);
|
|
console.log();
|
|
console.log('Review the plan and adjust as needed for your specific requirements.');
|
|
}
|
|
|
|
main();
|