test: add Playwright test infrastructure for session flow verification

Added comprehensive test infrastructure to verify session join flow and
track sync implementation:

Test Configuration:
- playwright.chrome.config.ts: Chrome-specific test configuration
- playwright.verification.config.ts: Verification test settings

Test Documentation:
- IMPLEMENTATION_APPROACH.md: TDD approach for trackSync
- JAM_UI_TESTING_PLAN.md: Overall testing strategy

Test Utilities:
- api-interceptor.ts: Intercepts and logs API calls
- websocket-monitor.ts: Monitors WebSocket messages
- sequence-comparator.ts: Compares API call sequences
- test-helpers.ts: Shared test helper functions

Test Suites:
- e2e/complete-session-flow.spec.ts: Full session flow E2E test
- api-verification/*.spec.ts: API call verification tests
- websocket-verification/ws-connection.spec.ts: WebSocket tests
- capture-session-flow*.spec.ts: Session flow analysis tests

Test Fixtures & Results:
- fixtures/legacy-sequences/: Recorded API call sequences
- test-results/: Test output, comparisons, and analysis

These tests were instrumental in debugging the VU meter issue and
verifying the trackSync implementation.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-23 15:42:35 +05:30
parent 23594959f3
commit b5f7f25698
40 changed files with 25241 additions and 0 deletions

View File

@ -0,0 +1,21 @@
import { PlaywrightTestConfig, devices } from '@playwright/test';
const config: PlaywrightTestConfig = {
// NO global setup for verification tests
use: {
baseURL: 'http://beta.jamkazam.local:4000',
actionTimeout: 10000,
headless: false,
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
video: 'retain-on-failure',
// Use Chrome
channel: 'chrome',
},
timeout: 120000, // 2 minutes per test
expect: {
timeout: 10000,
},
};
export default config;

View File

@ -0,0 +1,19 @@
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
// NO global setup for verification tests
use: {
baseURL: 'http://beta.jamkazam.local:4000',
actionTimeout: 10000,
headless: false,
viewport: { width: 1280, height: 720 },
ignoreHTTPSErrors: true,
video: 'retain-on-failure',
},
timeout: 120000, // 2 minutes per test
expect: {
timeout: 10000,
},
};
export default config;

View File

@ -0,0 +1,429 @@
# jam-ui Testing Implementation Approach
## How I Will Achieve the Goal
### Overview
You've asked me to verify that jam-ui conforms to the legacy app's API request/response sequence, patterns, and timings by creating comprehensive tests. Here's my structured approach:
## 🎯 Main Objectives
1. **API Sequence Verification** - Ensure jam-ui makes identical API calls as legacy app
2. **UI Testing** - Test all components function correctly
3. **E2E Testing** - Verify complete user journeys work end-to-end
4. **Comparison Analysis** - Generate reports showing conformance
## 📋 Implementation Strategy
### Phase 1: Test Infrastructure Setup
**What I'll Create:**
```
test/
├── utils/
│ ├── api-interceptor.ts # Intercept & record API calls
│ ├── websocket-monitor.ts # Monitor WebSocket connections
│ ├── sequence-comparator.ts # Compare API sequences
│ └── test-helpers.ts # Shared test utilities
├── fixtures/
│ ├── legacy-sequences/ # Copied from verification
│ │ ├── login-sequence.json
│ │ ├── session-creation-sequence.json
│ │ └── session-join-sequence.json
│ ├── users.json # Test user data
│ └── sessions.json # Test session data
```
**Purpose:** Reusable infrastructure for all tests
---
### Phase 2: API Verification Tests
**What I'll Create:**
```
test/api-verification/
├── login-api.spec.ts
├── session-creation-api.spec.ts
└── session-join-api.spec.ts
```
**How They Work:**
```typescript
// Example: login-api.spec.ts
test('jam-ui login API sequence matches legacy', async ({ page }) => {
const apiCalls = [];
// Intercept all API requests
page.on('request', req => {
if (req.url().includes('/api/')) {
apiCalls.push({
method: req.method(),
url: req.url(),
timestamp: Date.now()
});
}
});
// Perform login on jam-ui
await page.goto('http://beta.jamkazam.local:4000/');
await page.fill('[name="email"]', 'nuwan@jamkazam.com');
await page.fill('[name="password"]', 'jam123');
await page.click('button[type="submit"]');
await page.waitForLoadState('networkidle');
// Load expected sequence from legacy verification
const expectedSequence = require('../fixtures/legacy-sequences/login-sequence.json');
// Compare sequences
const comparison = compareAPISequences(apiCalls, expectedSequence);
// Report results
expect(comparison.matches).toBe(true);
if (!comparison.matches) {
console.log('Differences:', comparison.differences);
}
});
```
**What This Validates:**
- ✅ Same API endpoints called
- ✅ Same order of calls
- ✅ Similar timing between calls
- ✅ Request payloads match
- ✅ Responses handled correctly
---
### Phase 3: WebSocket Verification Tests
**What I'll Create:**
```
test/websocket-verification/
├── ws-connection.spec.ts
└── ws-messages.spec.ts
```
**How They Work:**
```typescript
test('jam-ui establishes correct WebSocket connections', async ({ page }) => {
const wsConnections = [];
page.on('websocket', ws => {
wsConnections.push({
url: ws.url(),
timestamp: Date.now()
});
ws.on('framereceived', frame => {
// Record messages
});
});
// Perform session join
await joinSession(page);
// Verify connections
expect(wsConnections).toHaveLength(2);
expect(wsConnections[0].url).toContain('localhost:3060');
expect(wsConnections[1].url).toContain('jamkazam.local:6767');
});
```
---
### Phase 4: UI Component Tests
**What I'll Create:**
```
test/unit/
├── LoginForm.test.tsx
├── SessionCreationForm.test.tsx
└── SessionInterface.test.tsx
```
**Framework:** React Testing Library
**Example:**
```typescript
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import LoginForm from '../../src/components/auth/LoginForm';
test('LoginForm submits credentials correctly', async () => {
const mockLogin = jest.fn();
render(<LoginForm onLogin={mockLogin} />);
fireEvent.change(screen.getByLabelText(/email/i), {
target: { value: 'nuwan@jamkazam.com' }
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: 'jam123' }
});
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
email: 'nuwan@jamkazam.com',
password: 'jam123'
});
});
});
```
**What This Validates:**
- ✅ Forms render correctly
- ✅ User interactions work
- ✅ Validation functions properly
- ✅ Error messages display
- ✅ Loading states work
---
### Phase 5: E2E Integration Tests
**What I'll Create:**
```
test/e2e/
├── complete-session-flow.spec.ts
├── login-flow.spec.ts
└── session-creation-flow.spec.ts
```
**How They Work:**
```typescript
test('Complete session flow: login → create → join', async ({ page }) => {
// Step 1: Login
await page.goto('http://beta.jamkazam.local:4000/');
await page.fill('[name="email"]', 'nuwan@jamkazam.com');
await page.fill('[name="password"]', 'jam123');
await page.click('button[type="submit"]');
await expect(page).toHaveURL(/profile|dashboard/);
// Step 2: Navigate to session creation
await page.click('text=create session'); // Or appropriate selector
await expect(page).toHaveURL(/session.*create/);
// Step 3: Fill session form
await page.fill('[name="session_name"]', 'Test Session');
await page.selectOption('[name="session_type"]', 'private');
await page.click('button[type="submit"]');
// Step 4: Verify session interface loads
await expect(page.locator('text=audio inputs')).toBeVisible();
await expect(page.locator('text=personal mix')).toBeVisible();
await expect(page.locator('text=LEAVE')).toBeVisible();
// Take screenshot for verification
await page.screenshot({ path: 'test-results/e2e-session-complete.png' });
});
```
---
### Phase 6: Comparison & Reporting
**What I'll Create:**
```
test/utils/sequence-comparator.ts
```
**Features:**
- Compare API call sequences
- Generate visual diff reports
- Highlight missing/extra calls
- Show timing differences
- Export to HTML/JSON
**Example Report:**
```
API Sequence Comparison Report
================================
Login Flow:
✅ POST /api/sessions - MATCH
✅ GET /api/users/{id} - MATCH
❌ GET /api/shopping_carts - MISSING in jam-ui
⚠️ GET /api/genres - Called later than expected (delay: 200ms)
✅ GET /api/countries - MATCH
Session Creation Flow:
✅ POST /api/sessions - MATCH
❌ POST /api/sessions/{id}/participants - MISSING
✅ GET /api/sessions/{id} - MATCH
Summary:
Total API calls: 68/70 (2 missing)
Sequence match: 85%
Timing variance: ±150ms average
```
---
## 🔧 Technical Implementation Details
### Test Execution Flow
```
1. Load legacy baseline sequences from fixtures
2. Start jam-ui application at beta.jamkazam.local:4000
3. Execute test scenario (login, create session, join)
4. Capture all API calls and WebSocket messages
5. Compare with legacy baseline
6. Generate comparison report
7. Assert conformance criteria
```
### Key Utilities
**1. API Interceptor** (`test/utils/api-interceptor.ts`)
```typescript
export class APIInterceptor {
private calls: APICall[] = [];
intercept(page: Page) {
page.on('request', req => this.recordRequest(req));
page.on('response', res => this.recordResponse(res));
}
getCalls(): APICall[] {
return this.calls;
}
compareWith(expected: APICall[]): ComparisonResult {
// Comparison logic
}
}
```
**2. WebSocket Monitor** (`test/utils/websocket-monitor.ts`)
```typescript
export class WebSocketMonitor {
private connections: WSConnection[] = [];
monitor(page: Page) {
page.on('websocket', ws => {
this.recordConnection(ws);
ws.on('framereceived', frame => this.recordMessage(frame));
ws.on('framesent', frame => this.recordMessage(frame));
});
}
getConnections(): WSConnection[] {
return this.connections;
}
}
```
**3. Sequence Comparator** (`test/utils/sequence-comparator.ts`)
```typescript
export function compareAPISequences(
actual: APICall[],
expected: APICall[]
): ComparisonResult {
return {
matches: checkOrderAndContent(actual, expected),
missing: findMissingCalls(actual, expected),
extra: findExtraCalls(actual, expected),
outOfOrder: findOutOfOrderCalls(actual, expected),
timingVariance: calculateTimingVariance(actual, expected),
report: generateHTMLReport(actual, expected)
};
}
```
---
## 📦 Deliverables
### 1. Test Files
- ✅ 15-20 test specification files
- ✅ Test utilities and helpers
- ✅ Fixtures with legacy baseline data
### 2. Reports
- ✅ HTML comparison reports
- ✅ JSON data exports
- ✅ Screenshots at each step
- ✅ HAR files for network traffic
### 3. Documentation
- ✅ Test execution guide
- ✅ Failure analysis
- ✅ Recommendations for fixes
---
## ⏱️ Execution Timeline
**Immediate (Now):**
1. Create test infrastructure
2. Set up API interceptors
3. Create fixtures from verification data
**Phase 1 (30 mins):**
- API verification tests for login
**Phase 2 (1 hour):**
- API verification tests for session creation/join
- WebSocket tests
**Phase 3 (1 hour):**
- UI component tests
**Phase 4 (1 hour):**
- E2E tests
**Phase 5 (30 mins):**
- Generate reports
- Document findings
**Total Estimated Time:** 4 hours
---
## 📊 Success Metrics
### API Conformance
- **Target:** 95%+ API calls match
- **Threshold:** Missing < 5% of calls
- **Timing:** Within ±500ms variance
### WebSocket Conformance
- **Target:** Both connections established
- **Threshold:** Message count within 10% of legacy
### UI Functionality
- **Target:** 100% tests passing
- **Threshold:** No critical failures
### E2E Flow
- **Target:** Complete flow succeeds
- **Threshold:** All assertions pass
---
## 🚀 Getting Started
Once you approve this approach, I will:
1. ✅ Create all test infrastructure files
2. ✅ Implement API verification tests
3. ✅ Implement UI unit tests
4. ✅ Implement E2E tests
5. ✅ Run all tests against jam-ui
6. ✅ Generate comparison reports
7. ✅ Provide detailed findings and recommendations
**Ready to proceed?** I can start implementing immediately.
---
**Document Status:** Awaiting Approval
**Estimated Implementation Time:** 4 hours
**Expected Outcome:** Comprehensive test suite comparing jam-ui with legacy app

View File

@ -0,0 +1,272 @@
# jam-ui Testing Plan - Session Flow Verification
**Date:** ${new Date().toLocaleString()}
**Objective:** Verify jam-ui conforms to legacy app API sequence, patterns, and timings
## Overview
This testing plan compares the new jam-ui React application with the legacy Rails application to ensure:
1. API calls match in sequence, timing, and payloads
2. WebSocket connections are established correctly
3. UI components function as expected
4. End-to-end flow works seamlessly
## Testing Layers
### 1. API Sequence Verification Tests
**Purpose:** Ensure jam-ui makes the exact same API calls as the legacy app
**Approach:**
- Intercept all API requests during user flow
- Compare against captured legacy API sequence
- Verify request order, methods, URLs, and payloads
- Check response handling
**Test Files:**
- `test/api-verification/login-api.spec.ts`
- `test/api-verification/session-creation-api.spec.ts`
- `test/api-verification/session-join-api.spec.ts`
### 2. WebSocket Verification Tests
**Purpose:** Ensure WebSocket connections match legacy behavior
**Approach:**
- Monitor WebSocket connection establishment
- Verify connection URLs and parameters
- Check message sequences
- Validate dual connections (native + server)
**Test Files:**
- `test/websocket-verification/ws-connection.spec.ts`
- `test/websocket-verification/ws-messages.spec.ts`
### 3. UI Component Tests
**Purpose:** Test individual UI components in isolation
**Approach:**
- Unit tests for Login, Session Creation, Session Interface components
- Test user interactions
- Validate form submissions
- Check error handling
**Test Files:**
- `test/unit/LoginForm.test.tsx` (using React Testing Library)
- `test/unit/SessionCreationForm.test.tsx`
- `test/unit/SessionInterface.test.tsx`
### 4. E2E Integration Tests
**Purpose:** Test complete user journeys
**Approach:**
- Full flow from login to session join
- Real browser automation with Playwright
- Visual verification with screenshots
- Performance timing checks
**Test Files:**
- `test/e2e/complete-session-flow.spec.ts`
- `test/e2e/login-flow.spec.ts`
- `test/e2e/session-creation-flow.spec.ts`
## Test Environment
**jam-ui Application:**
- URL: http://beta.jamkazam.local:4000/
- Credentials: nuwan@jamkazam.com / jam123
**Legacy Application:**
- URL: http://www.jamkazam.local:3100/
- Used for baseline comparison
## Expected API Sequences (from verification)
### Login Flow (12 API calls)
```
POST /api/sessions (or similar login endpoint)
GET /api/users/{user_id}
GET /api/countries
GET /api/shopping_carts
GET /api/jamtracks/purchased
GET /api/teacher_distributions
GET /api/regions
GET /api/genres
GET /api/users/{user_id}/broadcast_notification
```
### Session Creation Flow (38 API calls)
```
# Dashboard reload (Ctrl+Shift+0 or equivalent)
GET /api/users/{user_id} (2x)
GET /api/countries
GET /api/shopping_carts
GET /api/genres (3x)
GET /api/instruments (3x)
GET /api/users/{user_id}/friends (2x)
GET /api/jamtracks/purchased (3x)
GET /api/languages
GET /api/subjects
GET /api/chat (2x)
GET /api/sessions/scheduled
GET /api/users/{user_id}/notifications
GET /api/regions
GET /api/teacher_distributions
GET /api/versioncheck (3x)
GET /api/config/client
# Session creation
POST /api/sessions
POST /api/sessions/{session_id}/participants
GET /api/sessions/{session_id}/history
GET /api/sessions/{session_id} (3x)
PUT /api/sessions/{session_id}/tracks (3x)
GET /api/chat?...&channel=session&music_session={session_id}
POST /api/users/{user_id}/udp_reachable
GET /api/users/{user_id}/broadcast_notification
```
### WebSocket Connections
```
1. Dashboard connection: ws://www.jamkazam.local:6767/websocket?...
2. Native client: ws://localhost:3060/
3. Session connection: ws://www.jamkazam.local:6767/websocket?...&client_id=...
```
## Test Implementation Strategy
### Phase 1: Setup & Infrastructure (Day 1)
- [ ] Set up test utilities and helpers
- [ ] Create API interception utilities
- [ ] Create WebSocket monitoring utilities
- [ ] Set up test data fixtures
- [ ] Create comparison utilities for API sequences
### Phase 2: API Verification Tests (Day 1-2)
- [ ] Login API sequence test
- [ ] Session creation API sequence test
- [ ] Session join API sequence test
- [ ] WebSocket connection tests
- [ ] WebSocket message sequence tests
### Phase 3: UI Component Tests (Day 2)
- [ ] LoginForm unit tests
- [ ] Session creation form unit tests
- [ ] Session interface component tests
### Phase 4: E2E Tests (Day 3)
- [ ] Complete login flow E2E
- [ ] Complete session creation flow E2E
- [ ] Complete session join flow E2E
- [ ] Error scenarios and edge cases
### Phase 5: Documentation & Reporting (Day 3)
- [ ] Test execution guide
- [ ] Failure analysis reports
- [ ] Recommendations for fixes
## Success Criteria
### API Compliance
- ✅ All API calls in correct order
- ✅ Request payloads match legacy
- ✅ Response handling is correct
- ✅ Error cases handled properly
### WebSocket Compliance
- ✅ Connections established to correct endpoints
- ✅ Connection parameters match legacy
- ✅ Message sequences match
- ✅ Dual connections work correctly
### UI Functionality
- ✅ All forms submit correctly
- ✅ Navigation works as expected
- ✅ Error messages display properly
- ✅ Loading states work correctly
### E2E Flow
- ✅ User can log in successfully
- ✅ User can create a session
- ✅ User can join a session
- ✅ Session interface loads completely
- ✅ No console errors
## Test Execution
### Run All Tests
```bash
npm test
```
### Run Specific Test Suites
```bash
# API verification only
npm run test:api
# UI component tests only
npm run test:unit
# E2E tests only
npm run test:e2e
# WebSocket tests only
npm run test:ws
```
### Run with Coverage
```bash
npm run test:coverage
```
### Run in CI/CD
```bash
npm run test:ci
```
## Test Data Management
### User Accounts
- Primary test user: nuwan@jamkazam.com
- Additional test users from `test/data/users.js`
### Session Data
- Test session templates in `test/fixtures/sessions.json`
- Mock API responses in `test/fixtures/api-responses.json`
### Legacy Baseline
- API sequences captured in `test-results/step-verification/`
- Use as reference for comparison
## Reporting
### Test Reports Location
- HTML reports: `test-results/html/`
- JSON reports: `test-results/json/`
- Screenshots: `test-results/screenshots/`
- HAR files: `test-results/har/`
- Comparison reports: `test-results/comparison/`
### Metrics Tracked
1. API call count per flow
2. API call timing/latency
3. WebSocket message count
4. WebSocket connection timing
5. UI rendering time
6. E2E flow completion time
7. Error rates
## Next Steps
1. Review this plan
2. Create test utility infrastructure
3. Implement API verification tests
4. Implement UI unit tests
5. Implement E2E tests
6. Run tests and generate reports
7. Analyze discrepancies
8. Provide recommendations for fixes
---
**Status:** Planning Phase
**Estimated Effort:** 3 days
**Priority:** High

View File

@ -0,0 +1,614 @@
/**
* 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();

View File

@ -0,0 +1,59 @@
const fs = require('fs');
const path = require('path');
const stepsDir = path.join(__dirname, '../test-results/step-verification');
console.log('='.repeat(80));
console.log('STEP-BY-STEP API CALL ANALYSIS');
console.log('='.repeat(80));
console.log();
for (let i = 1; i <= 5; i++) {
const stepDir = path.join(stepsDir, `step-${i}`);
const apiFile = path.join(stepDir, 'api-calls.json');
const wsFile = path.join(stepDir, 'ws-messages.json');
if (!fs.existsSync(apiFile)) {
console.log(`Step ${i}: No data captured (step not completed)`);
console.log();
continue;
}
const apiCalls = JSON.parse(fs.readFileSync(apiFile, 'utf-8'));
const wsMessages = JSON.parse(fs.readFileSync(wsFile, 'utf-8'));
const requests = apiCalls.filter(c => c.type === 'request');
const uniqueEndpoints = new Set();
requests.forEach(r => {
try {
const url = new URL(r.url);
uniqueEndpoints.add(`${r.method} ${url.pathname}`);
} catch (e) {
// Skip invalid URLs
}
});
console.log(`STEP ${i}:`);
console.log(` Total API requests: ${requests.length}`);
console.log(` Unique endpoints: ${uniqueEndpoints.size}`);
console.log(` WebSocket messages: ${wsMessages.length}`);
console.log();
console.log(' Unique API endpoints called:');
Array.from(uniqueEndpoints).forEach(endpoint => {
const count = requests.filter(r => {
try {
const url = new URL(r.url);
return `${r.method} ${url.pathname}` === endpoint;
} catch (e) {
return false;
}
}).length;
console.log(` ${endpoint}${count > 1 ? ` (${count} times)` : ''}`);
});
console.log();
console.log('-'.repeat(80));
console.log();
}

View File

@ -0,0 +1,148 @@
import { test, expect } from '@playwright/test';
import { APIInterceptor } from '../utils/api-interceptor';
import { compareAPISequences } from '../utils/sequence-comparator';
import { loginToJamUI, waitForAPICalls } from '../utils/test-helpers';
import * as fs from 'fs';
import * as path from 'path';
test.describe('Login API Verification', () => {
test('jam-ui login makes same API calls as legacy app', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
// Perform login
await loginToJamUI(page, {
email: 'nuwan@jamkazam.com',
password: 'jam123',
});
await waitForAPICalls(page, 5000);
// Get captured calls
const actualCalls = apiInterceptor.getCalls();
// Load expected sequence
const expectedPath = path.join(__dirname, '../fixtures/legacy-sequences/login-simplified.json');
const expectedCalls = JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
console.log(`\nLogin API Verification:`);
console.log(` Actual calls: ${actualCalls.length}`);
console.log(` Expected calls: ${expectedCalls.length}`);
// Compare sequences
const comparison = compareAPISequences(actualCalls, expectedCalls);
// Log comparison report
console.log('\n' + comparison.report);
// Save results
const resultsDir = path.join(__dirname, '../test-results/api-verification');
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
fs.writeFileSync(
path.join(resultsDir, 'login-actual-calls.json'),
JSON.stringify(actualCalls, null, 2)
);
fs.writeFileSync(
path.join(resultsDir, 'login-comparison-report.md'),
comparison.report
);
// Assert that we have critical login endpoints
const endpoints = apiInterceptor.getUniqueEndpoints();
// Critical endpoints that MUST be present
expect(endpoints.some(e => e.includes('/api/users/'))).toBe(true);
expect(endpoints.some(e => e.includes('/api/genres'))).toBe(true);
expect(endpoints.some(e => e.includes('/api/countries'))).toBe(true);
// Check if match percentage is acceptable
const matchPercentage = (comparison.matchedCalls / comparison.totalCalls) * 100;
console.log(`\nMatch percentage: ${matchPercentage.toFixed(1)}%`);
// We expect at least 80% match for login flow
expect(matchPercentage).toBeGreaterThanOrEqual(80);
});
test('login API calls are in correct order', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
const actualCalls = apiInterceptor.getCalls();
const expectedPath = path.join(__dirname, '../fixtures/legacy-sequences/login-simplified.json');
const expectedCalls = JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
const comparison = compareAPISequences(actualCalls, expectedCalls);
// Check for significant order issues
if (comparison.outOfOrderCalls.length > 0) {
console.log('\nOut of order calls detected:');
for (const order of comparison.outOfOrderCalls) {
console.log(` ${order.endpoint}: deviation of ${order.deviation} positions`);
}
}
// Allow some flexibility in ordering, but not too much
const maxDeviationAllowed = 5;
const significantDeviations = comparison.outOfOrderCalls.filter(
o => o.deviation > maxDeviationAllowed
);
expect(significantDeviations.length).toBe(0);
});
test('login makes POST request to create session', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
const postCalls = apiInterceptor.getCallsByMethod('POST');
// Should have POST to /api/sessions or similar login endpoint
const sessionPost = postCalls.find(call =>
call.pathname.includes('/api/session') ||
call.pathname.includes('/api/auth') ||
call.pathname.includes('/api/login')
);
// Note: Legacy app uses Rails sessions, so this might not be present
// Just log what we find
console.log('\nPOST calls during login:');
postCalls.forEach(call => {
console.log(` ${call.method} ${call.pathname}`);
});
});
test('login retrieves user data', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
const userCalls = apiInterceptor.getCallsByPath('/api/users/');
expect(userCalls.length).toBeGreaterThan(0);
// Should have successful response
const successfulUserCall = userCalls.find(call =>
call.responseStatus && call.responseStatus >= 200 && call.responseStatus < 300
);
expect(successfulUserCall).toBeDefined();
if (successfulUserCall?.responseBody) {
console.log('\nUser data received:');
console.log(` User ID: ${successfulUserCall.responseBody.id || 'N/A'}`);
console.log(` Email: ${successfulUserCall.responseBody.email || 'N/A'}`);
}
});
});

View File

@ -0,0 +1,207 @@
import { test, expect } from '@playwright/test';
import { APIInterceptor } from '../utils/api-interceptor';
import { compareAPISequences } from '../utils/sequence-comparator';
import { loginToJamUI, navigateToSessionCreation, fillSessionForm, waitForAPICalls } from '../utils/test-helpers';
import * as fs from 'fs';
import * as path from 'path';
test.describe('Session Creation API Verification', () => {
test('session creation makes all required API calls', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
// Login first
await loginToJamUI(page);
await waitForAPICalls(page);
// Clear captured calls from login
apiInterceptor.reset();
// Navigate to session creation and fill form (jam-ui approach)
await navigateToSessionCreation(page);
await waitForAPICalls(page);
// Fill and submit session creation form
await fillSessionForm(page, { sessionType: 'private' });
await waitForAPICalls(page, 5000);
// Get captured calls
const actualCalls = apiInterceptor.getCalls();
// Load expected sequence (Step 5 from verification)
const expectedPath = path.join(__dirname, '../fixtures/legacy-sequences/session-creation-simplified.json');
const expectedCalls = JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
console.log(`\nSession Creation API Verification:`);
console.log(` Actual calls: ${actualCalls.length}`);
console.log(` Expected calls: ${expectedCalls.length}`);
// Compare sequences
const comparison = compareAPISequences(actualCalls, expectedCalls);
// Log comparison report
console.log('\n' + comparison.report);
// Save results
const resultsDir = path.join(__dirname, '../test-results/api-verification');
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
fs.writeFileSync(
path.join(resultsDir, 'session-creation-actual-calls.json'),
JSON.stringify(actualCalls, null, 2)
);
fs.writeFileSync(
path.join(resultsDir, 'session-creation-comparison-report.md'),
comparison.report
);
// Critical session creation endpoints
const endpoints = apiInterceptor.getUniqueEndpoints();
// Must have session creation
expect(endpoints.some(e => e.includes('POST /api/sessions'))).toBe(true);
// Check match percentage
const matchPercentage = (comparison.matchedCalls / comparison.totalCalls) * 100;
console.log(`\nMatch percentage: ${matchPercentage.toFixed(1)}%`);
// We expect at least 70% match (allowing for some differences)
expect(matchPercentage).toBeGreaterThanOrEqual(70);
});
test('session creation sequence includes critical endpoints', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
apiInterceptor.reset();
await navigateToSessionCreation(page);
await waitForAPICalls(page);
await fillSessionForm(page);
await waitForAPICalls(page, 5000);
const calls = apiInterceptor.getCalls();
// Find critical session endpoints
const sessionCreation = calls.find(c =>
c.method === 'POST' && c.pathname.match(/\/api\/sessions\/?$/)
);
const participantAdd = calls.find(c =>
c.method === 'POST' && c.pathname.includes('/participants')
);
const sessionHistory = calls.find(c =>
c.method === 'GET' && c.pathname.includes('/history')
);
const sessionGet = calls.find(c =>
c.method === 'GET' && c.pathname.match(/\/api\/sessions\/[a-f0-9-]+\/?$/)
);
const tracksUpdate = calls.find(c =>
c.method === 'PUT' && c.pathname.includes('/tracks')
);
const sessionChat = calls.find(c =>
c.method === 'GET' &&
c.pathname.includes('/chat') &&
c.url.includes('music_session')
);
const udpReachable = calls.find(c =>
c.method === 'POST' && c.pathname.includes('/udp_reachable')
);
console.log('\nCritical session endpoints found:');
console.log(` ✓ POST /api/sessions: ${sessionCreation ? 'YES' : 'NO'}`);
console.log(` ✓ POST /api/sessions/{id}/participants: ${participantAdd ? 'YES' : 'NO'}`);
console.log(` ✓ GET /api/sessions/{id}/history: ${sessionHistory ? 'YES' : 'NO'}`);
console.log(` ✓ GET /api/sessions/{id}: ${sessionGet ? 'YES' : 'NO'}`);
console.log(` ✓ PUT /api/sessions/{id}/tracks: ${tracksUpdate ? 'YES' : 'NO'}`);
console.log(` ✓ GET /api/chat?...music_session=: ${sessionChat ? 'YES' : 'NO'}`);
console.log(` ✓ POST /api/users/{id}/udp_reachable: ${udpReachable ? 'YES' : 'NO'}`);
// All critical endpoints should be present
expect(sessionCreation).toBeDefined();
expect(participantAdd).toBeDefined();
expect(sessionHistory).toBeDefined();
expect(sessionGet).toBeDefined();
expect(tracksUpdate).toBeDefined();
expect(udpReachable).toBeDefined();
});
test('session is created with valid session ID', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
apiInterceptor.reset();
await navigateToSessionCreation(page);
await waitForAPICalls(page);
await fillSessionForm(page);
await waitForAPICalls(page, 5000);
const calls = apiInterceptor.getCalls();
// Find the POST /api/sessions call
const sessionCreation = calls.find(c =>
c.method === 'POST' && c.pathname.match(/\/api\/sessions\/?$/)
);
expect(sessionCreation).toBeDefined();
// Check if response contains session ID
if (sessionCreation?.responseBody) {
const sessionId = sessionCreation.responseBody.id || sessionCreation.responseBody.session_id;
console.log(`\nSession created with ID: ${sessionId}`);
// Verify it's a valid UUID
expect(sessionId).toMatch(/^[a-f0-9-]{36}$/i);
// Verify subsequent calls use this session ID
const sessionSpecificCalls = calls.filter(c =>
c.pathname.includes(sessionId)
);
console.log(`Subsequent calls using session ID: ${sessionSpecificCalls.length}`);
expect(sessionSpecificCalls.length).toBeGreaterThan(0);
}
});
test('session form loads reference data', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await waitForAPICalls(page);
apiInterceptor.reset();
await navigateToSessionCreation(page);
await waitForAPICalls(page, 3000);
const calls = apiInterceptor.getCalls();
// Check what reference data is loaded for the session creation form
const genres = calls.filter(c => c.pathname.includes('/api/genres'));
const instruments = calls.filter(c => c.pathname.includes('/api/instruments'));
const friends = calls.filter(c => c.pathname.includes('/friends'));
console.log('\nSession form API calls:');
console.log(` /api/genres: ${genres.length} calls`);
console.log(` /api/instruments: ${instruments.length} calls`);
console.log(` /api/users/{id}/friends: ${friends.length} calls`);
// jam-ui may load reference data differently than legacy
// Just verify the page loaded successfully
expect(calls.length).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,226 @@
import { test, expect } from '@playwright/test';
import { APIInterceptor } from '../utils/api-interceptor';
import { WebSocketMonitor } from '../utils/websocket-monitor';
import { loginToJamUI, navigateToSessionCreation, fillSessionForm, waitForAPICalls } from '../utils/test-helpers';
import * as fs from 'fs';
import * as path from 'path';
test.describe('Session Join Flow - Detailed API Comparison', () => {
test('capture complete session join API calls (jam-ui)', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
const wsMonitor = new WebSocketMonitor();
apiInterceptor.intercept(page);
wsMonitor.monitor(page);
console.log('\n========================================');
console.log('SESSION JOIN FLOW - API CAPTURE');
console.log('========================================\n');
// Login first
await loginToJamUI(page);
await waitForAPICalls(page, 3000);
// Navigate to session creation
await navigateToSessionCreation(page);
await waitForAPICalls(page, 2000);
console.log('Starting session creation + join flow...\n');
// Clear previous calls - we only want session join calls
apiInterceptor.reset();
const wsConnectionsBefore = wsMonitor.getConnectionCount();
// Create and join session
await fillSessionForm(page, { sessionType: 'private' });
await waitForAPICalls(page, 8000);
// Capture all data
const apiCalls = apiInterceptor.getCalls();
const wsConnections = wsMonitor.getConnections();
const newWsConnections = wsConnections.slice(wsConnectionsBefore);
console.log('========================================');
console.log('SESSION JOIN API CALLS (jam-ui)');
console.log('========================================\n');
// Group API calls by type
const sessionCreationCalls = apiCalls.filter(c =>
c.method === 'POST' && c.pathname.match(/\/api\/sessions\/?$/)
);
const participantCalls = apiCalls.filter(c =>
c.pathname.includes('/participants')
);
const sessionDetailCalls = apiCalls.filter(c =>
c.method === 'GET' && c.pathname.match(/\/api\/sessions\/[a-f0-9-]+\/?$/)
);
const sessionHistoryCalls = apiCalls.filter(c =>
c.pathname.includes('/history')
);
const trackCalls = apiCalls.filter(c =>
c.pathname.includes('/tracks')
);
const chatCalls = apiCalls.filter(c =>
c.pathname.includes('/chat') && c.url.includes('music_session')
);
const udpCalls = apiCalls.filter(c =>
c.pathname.includes('/udp_reachable')
);
const otherCalls = apiCalls.filter(c =>
!sessionCreationCalls.includes(c) &&
!participantCalls.includes(c) &&
!sessionDetailCalls.includes(c) &&
!sessionHistoryCalls.includes(c) &&
!trackCalls.includes(c) &&
!chatCalls.includes(c) &&
!udpCalls.includes(c)
);
console.log('REST API CALLS:\n');
console.log('1. Session Creation:');
sessionCreationCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
if (c.responseBody?.id) {
console.log(` Session ID: ${c.responseBody.id}`);
}
});
console.log('\n2. Add Participant (Join):');
participantCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
console.log('\n3. Get Session Details:');
sessionDetailCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
console.log('\n4. Get Session History:');
sessionHistoryCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
console.log('\n5. Update Tracks:');
if (trackCalls.length > 0) {
trackCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
} else {
console.log(' ❌ NOT CALLED');
}
console.log('\n6. Load Session Chat:');
if (chatCalls.length > 0) {
chatCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
} else {
console.log(' ❌ NOT CALLED');
}
console.log('\n7. UDP Reachability Check:');
if (udpCalls.length > 0) {
udpCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
} else {
console.log(' ❌ NOT CALLED');
}
console.log('\n8. Other API Calls:');
otherCalls.forEach(c => {
console.log(` ${c.method} ${c.pathname} -> ${c.responseStatus}`);
});
console.log('\n========================================');
console.log('WEBSOCKET CONNECTIONS');
console.log('========================================\n');
newWsConnections.forEach((conn, idx) => {
console.log(`Connection ${idx + 1}:`);
console.log(` URL: ${conn.url}`);
console.log(` Type: ${conn.isNativeClient ? 'Native Client' : conn.isServerConnection ? 'Server' : 'Other'}`);
console.log(` Messages: ${conn.messages.length}`);
console.log(` Status: ${conn.closed ? 'Closed' : 'Open'}`);
if (conn.url.includes('?')) {
const url = new URL(conn.url);
console.log(` Parameters:`);
url.searchParams.forEach((value, key) => {
console.log(` ${key}: ${value.substring(0, 50)}${value.length > 50 ? '...' : ''}`);
});
}
console.log('');
});
console.log('========================================');
console.log('SUMMARY');
console.log('========================================\n');
console.log(`Total REST API calls: ${apiCalls.length}`);
console.log(` - Session creation: ${sessionCreationCalls.length}`);
console.log(` - Add participant: ${participantCalls.length}`);
console.log(` - Get session details: ${sessionDetailCalls.length}`);
console.log(` - Get session history: ${sessionHistoryCalls.length}`);
console.log(` - Update tracks: ${trackCalls.length}`);
console.log(` - Session chat: ${chatCalls.length}`);
console.log(` - UDP reachability: ${udpCalls.length}`);
console.log(` - Other calls: ${otherCalls.length}`);
console.log(`\nWebSocket connections: ${newWsConnections.length}`);
console.log(`Total WebSocket messages: ${newWsConnections.reduce((sum, c) => sum + c.messages.length, 0)}`);
// Save detailed analysis
const resultsDir = path.join(__dirname, '../test-results/session-join-analysis');
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
const analysis = {
timestamp: new Date().toISOString(),
restApiCalls: {
total: apiCalls.length,
sessionCreation: sessionCreationCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
addParticipant: participantCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
getSessionDetails: sessionDetailCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
getSessionHistory: sessionHistoryCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
updateTracks: trackCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
sessionChat: chatCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
udpReachability: udpCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
other: otherCalls.map(c => ({ method: c.method, url: c.url, status: c.responseStatus })),
},
websocketConnections: newWsConnections.map(c => ({
url: c.url,
type: c.isNativeClient ? 'native' : c.isServerConnection ? 'server' : 'other',
messageCount: c.messages.length,
status: c.closed ? 'closed' : 'open',
})),
};
fs.writeFileSync(
path.join(resultsDir, 'jam-ui-session-join.json'),
JSON.stringify(analysis, null, 2)
);
fs.writeFileSync(
path.join(resultsDir, 'jam-ui-all-api-calls.json'),
JSON.stringify(apiCalls, null, 2)
);
fs.writeFileSync(
path.join(resultsDir, 'jam-ui-websockets.json'),
wsMonitor.toJSON()
);
console.log(`\n✓ Analysis saved to: ${resultsDir}`);
// Basic assertions
expect(sessionCreationCalls.length).toBeGreaterThan(0);
expect(participantCalls.length).toBeGreaterThan(0);
});
});

View File

@ -0,0 +1,366 @@
import { chromium } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
/**
* Standalone script to capture network traffic for the "Join Session" flow
* This runs independently without the global Playwright test setup
*
* Usage: node --loader ts-node/esm test/capture-session-flow-standalone.spec.ts
* Or: npx ts-node test/capture-session-flow-standalone.spec.ts
*/
async function captureSessionFlow() {
console.log('Starting session flow capture...\n');
// Launch browser with ability to capture network traffic
const browser = await chromium.launch({
headless: false, // Use headed mode to see what's happening
slowMo: 500, // Slow down by 500ms to make actions visible
});
const testResultsDir = path.join(__dirname, '../test-results');
if (!fs.existsSync(testResultsDir)) {
fs.mkdirSync(testResultsDir, { recursive: true });
}
const context = await browser.newContext({
ignoreHTTPSErrors: true,
recordHar: {
path: path.join(testResultsDir, 'session-join-flow.har'),
mode: 'full', // Capture full request/response bodies
},
});
const page = await context.newPage();
// Arrays to collect network activity
const wsMessages: any[] = [];
const apiCalls: any[] = [];
const allNetworkActivity: any[] = [];
// Listen to all network requests
page.on('request', request => {
const url = request.url();
const entry = {
step: 'REQUEST',
method: request.method(),
url: url,
headers: request.headers(),
timestamp: new Date().toISOString(),
};
if (url.includes('/api/')) {
console.log(`[API REQUEST] ${request.method()} ${url}`);
apiCalls.push(entry);
}
allNetworkActivity.push(entry);
});
page.on('response', async response => {
const url = response.url();
let body = null;
try {
const contentType = response.headers()['content-type'] || '';
if (contentType.includes('application/json') || contentType.includes('text/')) {
body = await response.text();
}
} catch (e) {
// Some responses can't be read
}
const entry = {
step: 'RESPONSE',
status: response.status(),
statusText: response.statusText(),
url: url,
headers: response.headers(),
body: body,
timestamp: new Date().toISOString(),
};
if (url.includes('/api/')) {
console.log(`[API RESPONSE] ${response.status()} ${url}`);
apiCalls.push(entry);
}
allNetworkActivity.push(entry);
});
// Listen to WebSocket events
page.on('websocket', ws => {
console.log(`[WEBSOCKET] Opened: ${ws.url()}`);
wsMessages.push({
event: 'open',
url: ws.url(),
timestamp: new Date().toISOString(),
});
ws.on('framesent', frame => {
console.log(`[WS SENT] ${frame.payload.toString().substring(0, 100)}...`);
wsMessages.push({
event: 'sent',
payload: frame.payload.toString(),
timestamp: new Date().toISOString(),
});
});
ws.on('framereceived', frame => {
console.log(`[WS RECEIVED] ${frame.payload.toString().substring(0, 100)}...`);
wsMessages.push({
event: 'received',
payload: frame.payload.toString(),
timestamp: new Date().toISOString(),
});
});
ws.on('close', () => {
console.log('[WEBSOCKET] Closed');
wsMessages.push({
event: 'close',
timestamp: new Date().toISOString(),
});
});
});
try {
console.log('\n=== STEP 1: Navigate to signin page ===');
await page.goto('http://www.jamkazam.local:3100/signin', {
waitUntil: 'networkidle',
timeout: 30000,
});
await page.waitForTimeout(2000);
console.log('\n=== STEP 2: Fill in login credentials ===');
// Try multiple selectors for email field
const emailSelectors = [
'input#session_email',
'input[name="email"]',
'input[type="email"]',
'input[placeholder*="email" i]',
];
let emailFilled = false;
for (const selector of emailSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.fill(selector, 'nuwan@jamkazam.com');
console.log(`Filled email using selector: ${selector}`);
emailFilled = true;
break;
}
} catch (e) {
continue;
}
}
if (!emailFilled) {
console.error('Could not find email input field!');
console.log('Available input fields:');
const inputs = await page.$$('input');
for (const input of inputs) {
const type = await input.getAttribute('type');
const name = await input.getAttribute('name');
const id = await input.getAttribute('id');
console.log(` - input: type=${type}, name=${name}, id=${id}`);
}
}
// Try multiple selectors for password field
const passwordSelectors = [
'input#session_password',
'input[name="password"]',
'input[type="password"]',
];
let passwordFilled = false;
for (const selector of passwordSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.fill(selector, 'jam123');
console.log(`Filled password using selector: ${selector}`);
passwordFilled = true;
break;
}
} catch (e) {
continue;
}
}
await page.waitForTimeout(1000);
console.log('\n=== STEP 3: Click sign in button ===');
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:has-text("Sign in")',
'button:has-text("Login")',
'button:has-text("Sign In")',
'input[value="Sign in"]',
'input[value="Login"]',
];
let submitClicked = false;
for (const selector of submitSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.click(selector);
console.log(`Clicked submit using selector: ${selector}`);
submitClicked = true;
break;
}
} catch (e) {
continue;
}
}
if (!submitClicked) {
console.error('Could not find submit button!');
console.log('Available buttons:');
const buttons = await page.$$('button, input[type="submit"]');
for (const button of buttons) {
const text = await button.textContent();
const type = await button.getAttribute('type');
const value = await button.getAttribute('value');
console.log(` - button: text="${text}", type=${type}, value=${value}`);
}
}
// Wait for navigation after login
console.log('Waiting for navigation after login...');
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {
console.log('networkidle timeout - continuing anyway');
});
await page.waitForTimeout(3000);
console.log('\n=== STEP 4: Press Cmd+Shift+0 to skip upgrade modal ===');
await page.keyboard.press('Meta+Shift+Digit0');
await page.waitForTimeout(1500);
console.log('Upgrade modal skipped');
console.log('\n=== STEP 5: Look for and click "Create Session" tile ===');
// Try multiple selectors for create session button
const createSessionSelectors = [
'[data-testid="create-session"]',
'button:has-text("Create Session")',
'a:has-text("Create Session")',
'div:has-text("Create Session")',
'.tile:has-text("Create Session")',
'[title*="Create Session"]',
];
let sessionClicked = false;
for (const selector of createSessionSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.click(selector);
console.log(`Clicked create session using selector: ${selector}`);
sessionClicked = true;
break;
}
} catch (e) {
continue;
}
}
if (!sessionClicked) {
console.warn('Could not find "Create Session" button - taking screenshot');
await page.screenshot({ path: path.join(testResultsDir, 'dashboard-screenshot.png'), fullPage: true });
}
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
console.log('networkidle timeout - continuing anyway');
});
await page.waitForTimeout(2000);
console.log('\n=== STEP 6: Press Control+Shift+0 to remove backdrop ===');
// Press Control+Shift+0 to trick browser as native client and remove backdrop
// This works even with the modal open
await page.keyboard.press('Control+Shift+Digit0');
await page.waitForTimeout(2000);
console.log('Backdrop removed, browser tricked as native client');
console.log('\n=== STEP 7: Click "Create Quick Start" button ===');
const quickStartSelectors = [
'button:has-text("Create Quick Start")',
'button:has-text("Quick Start")',
'button:has-text("Create quick start")',
'[data-testid="quick-start"]',
];
let quickStartClicked = false;
for (const selector of quickStartSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.click(selector);
console.log(`Clicked quick start using selector: ${selector}`);
quickStartClicked = true;
break;
}
} catch (e) {
continue;
}
}
if (!quickStartClicked) {
console.warn('Could not find "Create Quick Start" button - taking screenshot');
await page.screenshot({ path: path.join(testResultsDir, 'session-create-screenshot.png'), fullPage: true });
}
// Wait for session to load and WebSocket messages
console.log('\nWaiting for session to load and capturing WebSocket traffic...');
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {
console.log('networkidle timeout - continuing anyway');
});
await page.waitForTimeout(10000); // Give plenty of time for WebSocket messages
console.log('\n=== Recording Complete ===\n');
// Save all captured data
const apiCallsPath = path.join(testResultsDir, 'api-calls.json');
fs.writeFileSync(apiCallsPath, JSON.stringify(apiCalls, null, 2));
console.log(`✓ API calls saved to: ${apiCallsPath}`);
console.log(` Total API calls: ${apiCalls.length}`);
const wsMessagesPath = path.join(testResultsDir, 'websocket-messages.json');
fs.writeFileSync(wsMessagesPath, JSON.stringify(wsMessages, null, 2));
console.log(`✓ WebSocket messages saved to: ${wsMessagesPath}`);
console.log(` Total WebSocket messages: ${wsMessages.length}`);
const allActivityPath = path.join(testResultsDir, 'all-network-activity.json');
fs.writeFileSync(allActivityPath, JSON.stringify(allNetworkActivity, null, 2));
console.log(`✓ All network activity saved to: ${allActivityPath}`);
console.log(` Total network requests: ${allNetworkActivity.length}`);
// Take final screenshot
await page.screenshot({ path: path.join(testResultsDir, 'final-state.png'), fullPage: true });
console.log(`✓ Final screenshot saved`);
console.log('\nKeeping browser open for 10 seconds for manual inspection...');
await page.waitForTimeout(10000);
} catch (error) {
console.error('\n❌ Error during capture:', error);
await page.screenshot({ path: path.join(testResultsDir, 'error-screenshot.png'), fullPage: true });
throw error;
} finally {
console.log('\nClosing browser and saving HAR file...');
await context.close();
await browser.close();
console.log('✓ HAR file saved to: test-results/session-join-flow.har\n');
}
}
// Run the capture
captureSessionFlow()
.then(() => {
console.log('✅ Capture completed successfully!');
process.exit(0);
})
.catch((error) => {
console.error('❌ Capture failed:', error);
process.exit(1);
});

View File

@ -0,0 +1,165 @@
import { test, chromium } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
/**
* This test captures the complete network traffic (HAR file) for the "Join Session" flow
* from the legacy JamKazam application. The captured data will be used to:
* 1. Document all API calls to /api/*
* 2. Document all WebSocket messages
* 3. Create a migration test plan for the new jam-ui interface
*/
test('Capture network traffic for joining a music session', async () => {
// Launch browser with ability to capture network traffic
const browser = await chromium.launch({
headless: false, // Use headed mode to see what's happening
slowMo: 1000, // Slow down by 1 second to make actions visible
});
const context = await browser.newContext({
ignoreHTTPSErrors: true,
recordHar: {
path: path.join(__dirname, '../test-results/session-join-flow.har'),
mode: 'full', // Capture full request/response bodies
},
});
const page = await context.newPage();
// Array to collect all WebSocket messages
const wsMessages: any[] = [];
const apiCalls: any[] = [];
// Listen to all network requests
page.on('request', request => {
const url = request.url();
if (url.includes('/api/')) {
console.log(`API REQUEST: ${request.method()} ${url}`);
apiCalls.push({
type: 'request',
method: request.method(),
url: url,
headers: request.headers(),
timestamp: new Date().toISOString(),
});
}
});
page.on('response', async response => {
const url = response.url();
if (url.includes('/api/')) {
console.log(`API RESPONSE: ${response.status()} ${url}`);
let body = null;
try {
body = await response.text();
} catch (e) {
// Some responses can't be read
}
apiCalls.push({
type: 'response',
status: response.status(),
url: url,
headers: response.headers(),
body: body,
timestamp: new Date().toISOString(),
});
}
});
// Listen to WebSocket events
page.on('websocket', ws => {
console.log(`WebSocket opened: ${ws.url()}`);
wsMessages.push({
type: 'websocket-open',
url: ws.url(),
timestamp: new Date().toISOString(),
});
ws.on('framesent', frame => {
console.log(`WS SENT: ${frame.payload}`);
wsMessages.push({
type: 'sent',
payload: frame.payload,
timestamp: new Date().toISOString(),
});
});
ws.on('framereceived', frame => {
console.log(`WS RECEIVED: ${frame.payload}`);
wsMessages.push({
type: 'received',
payload: frame.payload,
timestamp: new Date().toISOString(),
});
});
ws.on('close', () => {
console.log('WebSocket closed');
wsMessages.push({
type: 'websocket-close',
timestamp: new Date().toISOString(),
});
});
});
try {
console.log('Step 1: Navigate to signin page');
await page.goto('http://www.jamkazam.com:3000/signin', {
waitUntil: 'networkidle',
timeout: 30000,
});
console.log('Step 2: Login with credentials');
// Fill in login form
await page.fill('input[name="email"], input[type="email"]', 'nuwan@jamkazam.com');
await page.fill('input[name="password"], input[type="password"]', 'jam123');
// Click sign in button
await page.click('button[type="submit"], input[type="submit"], button:has-text("Sign in"), button:has-text("Login")');
// Wait for navigation after login
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
console.log('Step 3: Click "create session" tile');
// Look for create session button/tile
await page.click('[data-testid="create-session"], button:has-text("Create Session"), a:has-text("Create Session")');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
console.log('Step 4: Press Control+Shift+0 to trick browser as native client');
await page.keyboard.press('Control+Shift+Digit0');
await page.waitForTimeout(1000);
console.log('Step 5: Click "create quick start" button');
await page.click('button:has-text("Create Quick Start"), button:has-text("Quick Start")');
// Wait for session to load
await page.waitForLoadState('networkidle');
await page.waitForTimeout(5000); // Give time for WebSocket messages
console.log('Recording complete. Saving captured data...');
// Save API calls to a separate JSON file
const apiCallsPath = path.join(__dirname, '../test-results/api-calls.json');
fs.writeFileSync(apiCallsPath, JSON.stringify(apiCalls, null, 2));
console.log(`API calls saved to: ${apiCallsPath}`);
// Save WebSocket messages to a separate JSON file
const wsMessagesPath = path.join(__dirname, '../test-results/websocket-messages.json');
fs.writeFileSync(wsMessagesPath, JSON.stringify(wsMessages, null, 2));
console.log(`WebSocket messages saved to: ${wsMessagesPath}`);
// Keep browser open for manual inspection
console.log('Keeping browser open for 30 seconds for manual inspection...');
await page.waitForTimeout(30000);
} catch (error) {
console.error('Error during test:', error);
throw error;
} finally {
// Close context to save HAR file
await context.close();
await browser.close();
}
});

View File

@ -0,0 +1,285 @@
import { test, expect } from '@playwright/test';
import { APIInterceptor } from '../utils/api-interceptor';
import { WebSocketMonitor } from '../utils/websocket-monitor';
import { compareAPISequences } from '../utils/sequence-comparator';
import {
loginToJamUI,
navigateToSessionCreation,
fillSessionForm,
verifySessionInterfaceLoaded,
waitForAPICalls,
extractSessionId,
} from '../utils/test-helpers';
import * as fs from 'fs';
import * as path from 'path';
test.describe('Complete Session Flow E2E', () => {
test('complete flow: login → create session → join session', async ({ page }) => {
const apiInterceptor = new APIInterceptor();
const wsMonitor = new WebSocketMonitor();
apiInterceptor.intercept(page);
wsMonitor.monitor(page);
console.log('\n========================================');
console.log('COMPLETE SESSION FLOW E2E TEST');
console.log('========================================\n');
// STEP 1: Login
console.log('Step 1: Login...');
await loginToJamUI(page, {
email: 'nuwan@jamkazam.com',
password: 'jam123',
});
await waitForAPICalls(page, 3000);
const loginCalls = apiInterceptor.getCalls().length;
console.log(` ✓ Logged in (${loginCalls} API calls)\n`);
// Verify we're logged in
await expect(page).not.toHaveURL(/signin|login/);
// STEP 2: Navigate to session creation
console.log('Step 2: Navigate to session creation...');
apiInterceptor.reset();
await navigateToSessionCreation(page);
await waitForAPICalls(page, 2000);
const navCalls = apiInterceptor.getCalls().length;
console.log(` ✓ Navigated to session creation (${navCalls} API calls)\n`);
// STEP 3: Enable native client and create session
// STEP 3: Create and join session
console.log('Step 3: Create and join session...');
apiInterceptor.reset();
await fillSessionForm(page, { sessionType: 'private' });
await waitForAPICalls(page, 8000);
const sessionCalls = apiInterceptor.getCalls();
console.log(` ✓ Session created (${sessionCalls.length} API calls)\n`);
// Verify session creation endpoints
const sessionCreation = sessionCalls.find(c =>
c.method === 'POST' && c.pathname.match(/\/api\/sessions\/?$/)
);
expect(sessionCreation).toBeDefined();
console.log(` ✓ POST /api/sessions called`);
const participantAdd = sessionCalls.find(c =>
c.method === 'POST' && c.pathname.includes('/participants')
);
expect(participantAdd).toBeDefined();
console.log(` ✓ POST /api/sessions/{id}/participants called`);
// Extract session ID
const sessionId = await extractSessionId(page);
if (sessionId) {
console.log(` ✓ Session ID: ${sessionId}`);
}
// Verify session interface loaded
console.log('\nStep 4: Verify session interface...');
await verifySessionInterfaceLoaded(page);
console.log(` ✓ Session interface elements present\n`);
// Verify WebSocket connections
const wsVerification = wsMonitor.verifyDualConnections();
console.log('Step 5: Verify WebSocket connections...');
console.log(` Native client: ${wsVerification.hasNativeClient ? 'YES' : 'NO'}`);
console.log(` Server: ${wsVerification.hasServerConnection ? 'YES' : 'NO'}`);
expect(wsVerification.bothEstablished).toBe(true);
console.log(` ✓ Dual connections established\n`);
// Compare with legacy sequence
console.log('Step 6: Compare with legacy sequence...');
const expectedPath = path.join(__dirname, '../fixtures/legacy-sequences/complete-flow-simplified.json');
const expectedCalls = JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
const allCalls = apiInterceptor.getCalls();
const comparison = compareAPISequences(allCalls, expectedCalls);
const matchPercentage = (comparison.matchedCalls / comparison.totalCalls) * 100;
console.log(` API calls: ${allCalls.length}`);
console.log(` Expected: ${expectedCalls.length}`);
console.log(` Match: ${matchPercentage.toFixed(1)}%`);
// Save comprehensive results
const resultsDir = path.join(__dirname, '../test-results/e2e');
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
// Save API calls
fs.writeFileSync(
path.join(resultsDir, 'complete-flow-api-calls.json'),
JSON.stringify(allCalls, null, 2)
);
// Save comparison report
fs.writeFileSync(
path.join(resultsDir, 'complete-flow-comparison.md'),
comparison.report
);
// Save WebSocket data
fs.writeFileSync(
path.join(resultsDir, 'complete-flow-websockets.json'),
wsMonitor.toJSON()
);
// Save screenshot
await page.screenshot({
path: path.join(resultsDir, 'session-interface.png'),
fullPage: true,
});
console.log('\n========================================');
console.log('COMPLETE SESSION FLOW TEST: PASSED');
console.log('========================================\n');
// Final assertions
expect(matchPercentage).toBeGreaterThanOrEqual(70);
expect(wsVerification.bothEstablished).toBe(true);
expect(sessionCreation).toBeDefined();
expect(participantAdd).toBeDefined();
});
test('session interface has all required controls', async ({ page }) => {
await loginToJamUI(page);
await waitForAPICalls(page);
await navigateToSessionCreation(page);
await waitForAPICalls(page);
await fillSessionForm(page);
await waitForAPICalls(page, 8000);
// Check for session control buttons
const expectedControls = [
'SETTINGS',
'VOLUME',
'BROADCAST',
'RECORD',
'VIDEO',
'FILES',
'RESYNC',
'LEAVE',
];
const foundControls: string[] = [];
const missingControls: string[] = [];
for (const control of expectedControls) {
const selectors = [
`text=${control}`,
`button:has-text("${control}")`,
`[data-testid="${control.toLowerCase()}"]`,
];
let found = false;
for (const selector of selectors) {
try {
const element = await page.$(selector);
if (element) {
foundControls.push(control);
found = true;
break;
}
} catch {
continue;
}
}
if (!found) {
missingControls.push(control);
}
}
console.log('\nSession interface controls:');
console.log(` Found: ${foundControls.join(', ')}`);
if (missingControls.length > 0) {
console.log(` Missing: ${missingControls.join(', ')}`);
}
// At least some key controls should be present
expect(foundControls.length).toBeGreaterThan(3);
});
test('session creation handles errors gracefully', async ({ page }) => {
// This test verifies error handling - might fail initially
const apiInterceptor = new APIInterceptor();
apiInterceptor.intercept(page);
await loginToJamUI(page);
await navigateToSessionCreation(page);
await page.keyboard.press('Control+Shift+Digit0');
await waitForAPICalls(page, 3000);
// Try to create session
await fillSessionForm(page);
await waitForAPICalls(page, 8000);
const calls = apiInterceptor.getCalls();
// Check if any calls failed
const failedCalls = calls.filter(c =>
c.responseStatus && c.responseStatus >= 400
);
if (failedCalls.length > 0) {
console.log('\nFailed API calls:');
failedCalls.forEach(call => {
console.log(` ${call.method} ${call.pathname}: ${call.responseStatus}`);
});
}
// Even if some calls fail, the critical session creation should succeed
const sessionCreation = calls.find(c =>
c.method === 'POST' &&
c.pathname.match(/\/api\/sessions\/?$/) &&
c.responseStatus &&
c.responseStatus >= 200 &&
c.responseStatus < 300
);
// Log whether session creation succeeded
console.log(`\nSession creation: ${sessionCreation ? 'SUCCESS' : 'FAILED'}`);
});
test('user can leave session', async ({ page }) => {
await loginToJamUI(page);
await waitForAPICalls(page);
await navigateToSessionCreation(page);
await waitForAPICalls(page);
await fillSessionForm(page);
await waitForAPICalls(page, 8000);
// Try to find and click LEAVE button
const leaveSelectors = [
'text=LEAVE',
'button:has-text("LEAVE")',
'[data-testid="leave-button"]',
'button:has-text("Leave Session")',
];
let leaveClicked = false;
for (const selector of leaveSelectors) {
try {
const element = await page.$(selector);
if (element) {
console.log(`\nFound LEAVE button: ${selector}`);
// Don't actually click it (would end the test), just verify it exists
leaveClicked = true;
break;
}
} catch {
continue;
}
}
console.log(`LEAVE button available: ${leaveClicked ? 'YES' : 'NO'}`);
expect(leaveClicked).toBe(true);
});
});

View File

@ -0,0 +1,103 @@
/**
* This script processes the raw step verification data and creates simplified
* fixture files for easier test comparison
*/
const fs = require('fs');
const path = require('path');
const stepDir = path.join(__dirname, '../../test-results/step-verification');
const fixturesDir = path.join(__dirname, 'legacy-sequences');
function processStepData(stepNumber, stepName) {
const stepPath = path.join(stepDir, `step-${stepNumber}`);
const apiCallsFile = path.join(stepPath, 'api-calls.json');
if (!fs.existsSync(apiCallsFile)) {
console.log(`⚠️ Step ${stepNumber} API calls not found`);
return;
}
const apiCalls = JSON.parse(fs.readFileSync(apiCallsFile, 'utf8'));
// Filter to just requests
const requests = apiCalls.filter(call => call.type === 'request');
// Create simplified version with just essential info
const simplified = requests.map((call, index) => {
const url = new URL(call.url);
return {
sequence: index,
method: call.method,
url: call.url,
pathname: url.pathname,
timestamp: call.timestamp,
endpoint: `${call.method} ${url.pathname}`,
};
});
// Save simplified version
const outputFile = path.join(fixturesDir, `${stepName}-simplified.json`);
fs.writeFileSync(outputFile, JSON.stringify(simplified, null, 2));
console.log(`✓ Created ${outputFile}`);
console.log(` ${simplified.length} API calls`);
return simplified;
}
console.log('Creating simplified fixtures from step verification data...\n');
// Process each step
const steps = [
{ number: 1, name: 'login' },
{ number: 2, name: 'dashboard' },
{ number: 3, name: 'skip-modal' },
{ number: 4, name: 'navigate-to-session' },
{ number: 5, name: 'session-creation' },
];
const allSequences = {};
for (const step of steps) {
const sequence = processStepData(step.number, step.name);
if (sequence) {
allSequences[step.name] = sequence;
}
}
// Create combined complete flow
const completeFlow = Object.values(allSequences).flat();
const completeFile = path.join(fixturesDir, 'complete-flow-simplified.json');
fs.writeFileSync(completeFile, JSON.stringify(completeFlow, null, 2));
console.log(`\n✓ Created ${completeFile}`);
console.log(` ${completeFlow.length} total API calls`);
// Create summary file with unique endpoints
const uniqueEndpoints = new Map();
for (const call of completeFlow) {
const endpoint = call.endpoint;
if (!uniqueEndpoints.has(endpoint)) {
uniqueEndpoints.set(endpoint, 0);
}
uniqueEndpoints.set(endpoint, uniqueEndpoints.get(endpoint) + 1);
}
const summary = {
totalCalls: completeFlow.length,
uniqueEndpoints: uniqueEndpoints.size,
endpoints: Array.from(uniqueEndpoints.entries()).map(([endpoint, count]) => ({
endpoint,
count,
})),
steps: Object.entries(allSequences).map(([name, calls]) => ({
step: name,
callCount: calls.length,
})),
};
const summaryFile = path.join(fixturesDir, 'summary.json');
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
console.log(`\n✓ Created ${summaryFile}`);
console.log(` ${summary.uniqueEndpoints} unique endpoints`);
console.log(` ${summary.totalCalls} total API calls`);

View File

@ -0,0 +1,562 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:25.212Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:31.522Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/countries",
"pathname": "/api/countries",
"timestamp": "2026-01-20T10:15:31.618Z",
"endpoint": "GET /api/countries"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/shopping_carts?time=1768904131749",
"pathname": "/api/shopping_carts",
"timestamp": "2026-01-20T10:15:31.756Z",
"endpoint": "GET /api/shopping_carts"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:31.921Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/teacher_distributions?per_page=20&page=1",
"pathname": "/api/teacher_distributions",
"timestamp": "2026-01-20T10:15:31.923Z",
"endpoint": "GET /api/teacher_distributions"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/regions?country=US",
"pathname": "/api/regions",
"timestamp": "2026-01-20T10:15:31.990Z",
"endpoint": "GET /api/regions"
},
{
"sequence": 7,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.727Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 8,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.727Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 9,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.938Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"timestamp": "2026-01-20T10:15:33.961Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification"
},
{
"sequence": 11,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:34.127Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.765Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.766Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.815Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:41.816Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications",
"timestamp": "2026-01-20T10:15:41.816Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 7,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 8,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/languages",
"pathname": "/api/languages",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/languages"
},
{
"sequence": 9,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/subjects",
"pathname": "/api/subjects",
"timestamp": "2026-01-20T10:15:41.928Z",
"endpoint": "GET /api/subjects"
},
{
"sequence": 11,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=global",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:41.928Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 12,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:42.826Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 13,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:42.829Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 14,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/healthcheck",
"pathname": "/api/healthcheck",
"timestamp": "2026-01-20T10:15:43.752Z",
"endpoint": "GET /api/healthcheck"
},
{
"sequence": 15,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/config/client?",
"pathname": "/api/config/client",
"timestamp": "2026-01-20T10:15:43.932Z",
"endpoint": "GET /api/config/client"
},
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:45.349Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/scheduled?",
"pathname": "/api/sessions/scheduled",
"timestamp": "2026-01-20T10:15:50.945Z",
"endpoint": "GET /api/sessions/scheduled"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:50.946Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:51.197Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:55.382Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/countries",
"pathname": "/api/countries",
"timestamp": "2026-01-20T10:15:55.437Z",
"endpoint": "GET /api/countries"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/shopping_carts?time=1768904155592",
"pathname": "/api/shopping_carts",
"timestamp": "2026-01-20T10:15:55.594Z",
"endpoint": "GET /api/shopping_carts"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.655Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.655Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.664Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 7,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 8,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 9,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.739Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/languages",
"pathname": "/api/languages",
"timestamp": "2026-01-20T10:15:55.739Z",
"endpoint": "GET /api/languages"
},
{
"sequence": 11,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 12,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/subjects",
"pathname": "/api/subjects",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/subjects"
},
{
"sequence": 13,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=global",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 14,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/scheduled?",
"pathname": "/api/sessions/scheduled",
"timestamp": "2026-01-20T10:15:55.987Z",
"endpoint": "GET /api/sessions/scheduled"
},
{
"sequence": 15,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:55.987Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 16,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:56.005Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 17,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications",
"timestamp": "2026-01-20T10:15:56.006Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications"
},
{
"sequence": 18,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/regions?country=US",
"pathname": "/api/regions",
"timestamp": "2026-01-20T10:15:56.009Z",
"endpoint": "GET /api/regions"
},
{
"sequence": 19,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:56.247Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 20,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/teacher_distributions?per_page=20&page=1",
"pathname": "/api/teacher_distributions",
"timestamp": "2026-01-20T10:15:56.247Z",
"endpoint": "GET /api/teacher_distributions"
},
{
"sequence": 21,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.251Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 22,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.254Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 23,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.305Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 24,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:57.016Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 25,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/sessions",
"pathname": "/api/sessions",
"timestamp": "2026-01-20T10:15:57.295Z",
"endpoint": "POST /api/sessions"
},
{
"sequence": 26,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history?includePending=false",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history",
"timestamp": "2026-01-20T10:15:57.677Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history"
},
{
"sequence": 27,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/config/client?",
"pathname": "/api/config/client",
"timestamp": "2026-01-20T10:15:57.748Z",
"endpoint": "GET /api/config/client"
},
{
"sequence": 28,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants",
"timestamp": "2026-01-20T10:15:57.929Z",
"endpoint": "POST /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants"
},
{
"sequence": 29,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"timestamp": "2026-01-20T10:15:58.794Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification"
},
{
"sequence": 30,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=session&music_session=02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:58.809Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 31,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:15:58.859Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 32,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:15:58.964Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
},
{
"sequence": 33,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:15:59.264Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 34,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:15:59.317Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
},
{
"sequence": 35,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable",
"timestamp": "2026-01-20T10:15:59.547Z",
"endpoint": "POST /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable"
},
{
"sequence": 36,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:16:05.013Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 37,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:16:05.049Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
}
]

View File

@ -0,0 +1,130 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.765Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.766Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.815Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:41.816Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications",
"timestamp": "2026-01-20T10:15:41.816Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 7,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 8,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/languages",
"pathname": "/api/languages",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/languages"
},
{
"sequence": 9,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:41.927Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/subjects",
"pathname": "/api/subjects",
"timestamp": "2026-01-20T10:15:41.928Z",
"endpoint": "GET /api/subjects"
},
{
"sequence": 11,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=global",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:41.928Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 12,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:42.826Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 13,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:42.829Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 14,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/healthcheck",
"pathname": "/api/healthcheck",
"timestamp": "2026-01-20T10:15:43.752Z",
"endpoint": "GET /api/healthcheck"
},
{
"sequence": 15,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/config/client?",
"pathname": "/api/config/client",
"timestamp": "2026-01-20T10:15:43.932Z",
"endpoint": "GET /api/config/client"
}
]

View File

@ -0,0 +1,98 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:25.212Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:31.522Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/countries",
"pathname": "/api/countries",
"timestamp": "2026-01-20T10:15:31.618Z",
"endpoint": "GET /api/countries"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/shopping_carts?time=1768904131749",
"pathname": "/api/shopping_carts",
"timestamp": "2026-01-20T10:15:31.756Z",
"endpoint": "GET /api/shopping_carts"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:31.921Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/teacher_distributions?per_page=20&page=1",
"pathname": "/api/teacher_distributions",
"timestamp": "2026-01-20T10:15:31.923Z",
"endpoint": "GET /api/teacher_distributions"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/regions?country=US",
"pathname": "/api/regions",
"timestamp": "2026-01-20T10:15:31.990Z",
"endpoint": "GET /api/regions"
},
{
"sequence": 7,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.727Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 8,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.727Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 9,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:33.938Z",
"endpoint": "POST /xoplatform/logger/api/logger"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"timestamp": "2026-01-20T10:15:33.961Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification"
},
{
"sequence": 11,
"method": "POST",
"url": "https://www.paypal.com/xoplatform/logger/api/logger",
"pathname": "/xoplatform/logger/api/logger",
"timestamp": "2026-01-20T10:15:34.127Z",
"endpoint": "POST /xoplatform/logger/api/logger"
}
]

View File

@ -0,0 +1,26 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/scheduled?",
"pathname": "/api/sessions/scheduled",
"timestamp": "2026-01-20T10:15:50.945Z",
"endpoint": "GET /api/sessions/scheduled"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:50.946Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:51.197Z",
"endpoint": "GET /api/jamtracks/purchased"
}
]

View File

@ -0,0 +1,306 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:55.382Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 1,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/countries",
"pathname": "/api/countries",
"timestamp": "2026-01-20T10:15:55.437Z",
"endpoint": "GET /api/countries"
},
{
"sequence": 2,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/shopping_carts?time=1768904155592",
"pathname": "/api/shopping_carts",
"timestamp": "2026-01-20T10:15:55.594Z",
"endpoint": "GET /api/shopping_carts"
},
{
"sequence": 3,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.655Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 4,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.655Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 5,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.664Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 6,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 7,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 8,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:55.734Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 9,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/instruments",
"pathname": "/api/instruments",
"timestamp": "2026-01-20T10:15:55.739Z",
"endpoint": "GET /api/instruments"
},
{
"sequence": 10,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/languages",
"pathname": "/api/languages",
"timestamp": "2026-01-20T10:15:55.739Z",
"endpoint": "GET /api/languages"
},
{
"sequence": 11,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/genres",
"pathname": "/api/genres",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/genres"
},
{
"sequence": 12,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/subjects",
"pathname": "/api/subjects",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/subjects"
},
{
"sequence": 13,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=global",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:55.740Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 14,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/scheduled?",
"pathname": "/api/sessions/scheduled",
"timestamp": "2026-01-20T10:15:55.987Z",
"endpoint": "GET /api/sessions/scheduled"
},
{
"sequence": 15,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381?",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"timestamp": "2026-01-20T10:15:55.987Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381"
},
{
"sequence": 16,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": "2026-01-20T10:15:56.005Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends"
},
{
"sequence": 17,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications",
"timestamp": "2026-01-20T10:15:56.006Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications"
},
{
"sequence": 18,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/regions?country=US",
"pathname": "/api/regions",
"timestamp": "2026-01-20T10:15:56.009Z",
"endpoint": "GET /api/regions"
},
{
"sequence": 19,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:56.247Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 20,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/teacher_distributions?per_page=20&page=1",
"pathname": "/api/teacher_distributions",
"timestamp": "2026-01-20T10:15:56.247Z",
"endpoint": "GET /api/teacher_distributions"
},
{
"sequence": 21,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.251Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 22,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.254Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 23,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:56.305Z",
"endpoint": "GET /api/versioncheck"
},
{
"sequence": 24,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/jamtracks/purchased?page=1&per_page=40",
"pathname": "/api/jamtracks/purchased",
"timestamp": "2026-01-20T10:15:57.016Z",
"endpoint": "GET /api/jamtracks/purchased"
},
{
"sequence": 25,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/sessions",
"pathname": "/api/sessions",
"timestamp": "2026-01-20T10:15:57.295Z",
"endpoint": "POST /api/sessions"
},
{
"sequence": 26,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history?includePending=false",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history",
"timestamp": "2026-01-20T10:15:57.677Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history"
},
{
"sequence": 27,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/config/client?",
"pathname": "/api/config/client",
"timestamp": "2026-01-20T10:15:57.748Z",
"endpoint": "GET /api/config/client"
},
{
"sequence": 28,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants",
"timestamp": "2026-01-20T10:15:57.929Z",
"endpoint": "POST /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants"
},
{
"sequence": 29,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"timestamp": "2026-01-20T10:15:58.794Z",
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification"
},
{
"sequence": 30,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/chat?type=CHAT_MESSAGE&limit=20&page=0&channel=session&music_session=02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/chat",
"timestamp": "2026-01-20T10:15:58.809Z",
"endpoint": "GET /api/chat"
},
{
"sequence": 31,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:15:58.859Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 32,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:15:58.964Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
},
{
"sequence": 33,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:15:59.264Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 34,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:15:59.317Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
},
{
"sequence": 35,
"method": "POST",
"url": "http://www.jamkazam.local:3100/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable",
"timestamp": "2026-01-20T10:15:59.547Z",
"endpoint": "POST /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable"
},
{
"sequence": 36,
"method": "PUT",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"timestamp": "2026-01-20T10:16:05.013Z",
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks"
},
{
"sequence": 37,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"pathname": "/api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"timestamp": "2026-01-20T10:16:05.049Z",
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23"
}
]

View File

@ -0,0 +1,10 @@
[
{
"sequence": 0,
"method": "GET",
"url": "http://www.jamkazam.local:3100/api/versioncheck?product=JamClientModern&os=MacOSX-M",
"pathname": "/api/versioncheck",
"timestamp": "2026-01-20T10:15:45.349Z",
"endpoint": "GET /api/versioncheck"
}
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
{
"totalCalls": 70,
"uniqueEndpoints": 25,
"endpoints": [
{
"endpoint": "GET /api/genres",
"count": 7
},
{
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381",
"count": 4
},
{
"endpoint": "GET /api/countries",
"count": 2
},
{
"endpoint": "GET /api/shopping_carts",
"count": 2
},
{
"endpoint": "GET /api/jamtracks/purchased",
"count": 5
},
{
"endpoint": "GET /api/teacher_distributions",
"count": 2
},
{
"endpoint": "GET /api/regions",
"count": 2
},
{
"endpoint": "POST /xoplatform/logger/api/logger",
"count": 4
},
{
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification",
"count": 2
},
{
"endpoint": "GET /api/instruments",
"count": 6
},
{
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"count": 4
},
{
"endpoint": "GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications",
"count": 2
},
{
"endpoint": "GET /api/languages",
"count": 2
},
{
"endpoint": "GET /api/subjects",
"count": 2
},
{
"endpoint": "GET /api/chat",
"count": 3
},
{
"endpoint": "GET /api/versioncheck",
"count": 6
},
{
"endpoint": "GET /api/healthcheck",
"count": 1
},
{
"endpoint": "GET /api/config/client",
"count": 2
},
{
"endpoint": "GET /api/sessions/scheduled",
"count": 2
},
{
"endpoint": "POST /api/sessions",
"count": 1
},
{
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history",
"count": 1
},
{
"endpoint": "POST /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants",
"count": 1
},
{
"endpoint": "PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks",
"count": 3
},
{
"endpoint": "GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23",
"count": 3
},
{
"endpoint": "POST /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable",
"count": 1
}
],
"steps": [
{
"step": "login",
"callCount": 12
},
{
"step": "dashboard",
"callCount": 16
},
{
"step": "skip-modal",
"callCount": 1
},
{
"step": "navigate-to-session",
"callCount": 3
},
{
"step": "session-creation",
"callCount": 38
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,109 @@
# API Sequence Comparison Report
## Summary
- **Match Status:** ❌ FAIL
- **Total Calls:** 13
- **Matched Calls:** 3
- **Match Percentage:** 23.1%
## ❌ Missing API Calls
The following expected API calls are missing or called fewer times:
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/shopping_carts**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/jamtracks/purchased**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/teacher_distributions**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **POST /xoplatform/logger/api/logger**
- Expected: 4 call(s)
- Actual: 0 call(s)
- Missing: 4 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
## ⚠️ Extra API Calls
The following API calls were made but not expected:
- **GET /api/app_features**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **GET /api/me**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **UNKNOWN /api/me**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **POST /api/auths/login**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/profile**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/instruments**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/cities**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
## ⚠️ Out of Order Calls
The following API calls occurred in a different order:
- **GET /api/genres**
- Expected position: 0
- Actual position: 10
- Deviation: 10 positions
- **GET /api/countries**
- Expected position: 2
- Actual position: 9
- Deviation: 7 positions
- **GET /api/regions**
- Expected position: 6
- Actual position: 11
- Deviation: 5 positions
## ❌ Conclusion
The API sequence does NOT match the expected baseline. Please review the mismatches above.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
# API Sequence Comparison Report
## Summary
- **Match Status:** ❌ FAIL
- **Total Calls:** 13
- **Matched Calls:** 4
- **Match Percentage:** 30.8%
## ❌ Missing API Calls
The following expected API calls are missing or called fewer times:
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/countries**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/shopping_carts**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/genres**
- Expected: 3 call(s)
- Actual: 1 call(s)
- Missing: 2 call(s)
- **GET /api/instruments**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/jamtracks/purchased**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/languages**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/subjects**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/chat**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/sessions/scheduled**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/regions**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/teacher_distributions**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/versioncheck**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/config/client**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **POST /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **POST /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
## ⚠️ Extra API Calls
The following API calls were made but not expected:
- **GET /api/me**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **GET /api/app_features**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/sessions/6278e6f5-013b-4cd2-8e54-061ea6306d66/history**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **POST /api/sessions/6278e6f5-013b-4cd2-8e54-061ea6306d66/participants**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/sessions/6278e6f5-013b-4cd2-8e54-061ea6306d66**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
## ⚠️ Out of Order Calls
The following API calls occurred in a different order:
- **GET /api/genres**
- Expected position: 4
- Actual position: 0
- Deviation: 4 positions
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 7
- Actual position: 2
- Deviation: 5 positions
- **GET /api/genres**
- Expected position: 11
- Actual position: 0
- Deviation: 11 positions
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 16
- Actual position: 2
- Deviation: 14 positions
- **POST /api/sessions**
- Expected position: 25
- Actual position: 4
- Deviation: 21 positions
## ❌ Conclusion
The API sequence does NOT match the expected baseline. Please review the mismatches above.

View File

@ -0,0 +1,772 @@
[
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions",
"pathname": "/api/sessions",
"timestamp": 1768927894879,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"requestBody": {
"privacy": "1",
"description": "Automated test session",
"inviteeIds": "",
"musician_access": true,
"approval_required": false,
"legal_terms": true,
"start": "Tue Jan 20 2026 10:21 PM",
"duration": "60",
"invitations": [],
"timezone": "Central Time (US & Canada),America/Chicago",
"genres": [
"acapella"
],
"friends_can_join": true,
"is_unstructured_rsvp": false,
"fan_chat": false,
"fan_access": false,
"legal_policy": "legal_policy",
"language": "eng",
"name": "my session",
"rsvp_slots": [
{
"instrument_id": "other",
"proficiency_level": 3,
"approve": true
}
]
},
"duration": 139,
"responseStatus": 201,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "ed02c69a-ca9c-4223-a4ce-1e7ef63ad112",
"x-runtime": "0.142518",
"etag": "W/\"1c90918fc0af71c877f432af9fddbff0\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"id": "c2682005-d64f-4f2e-9e72-b9017c0097fc",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-20T16:51:34.911Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "3b57a648-3858-44a0-8650-45555a6dd33b"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/ZAPOFY0FCPM",
"genres": [
"A Cappella"
],
"scheduled_start": "Tue 20 January 2026 22:21:00",
"pretty_scheduled_start_with_timezone": "Tuesday, January 20, 10:21-10:21 PM US Central Time",
"pretty_scheduled_start_short": "Tuesday, January 20 - 10:21pm"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/app_features?env=development",
"pathname": "/api/app_features",
"timestamp": 1768927895119,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 50,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "cdd88fd7-8a3f-4ae9-bf68-f2b9b772f29a",
"x-runtime": "0.046358",
"etag": "W/\"b7b5ed7616e7f6ac80c7b989a95ff930\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": []
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/me",
"pathname": "/api/me",
"timestamp": 1768927895119,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 66,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "d56f8566-0db9-432f-b346-fdde8316de03",
"x-runtime": "0.020159",
"etag": "W/\"39d0929eb10af68175337821ac59ae6c\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"email": "nuwan@jamkazam.com",
"photo_url": null,
"show_free_jamtrack": false,
"is_affiliate_partner": false,
"recording_pref": 1
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications",
"timestamp": 1768927895217,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 43,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "fa2daac0-4c35-401d-9fb3-24bde47fdffd",
"x-runtime": "0.053999",
"etag": "W/\"0457d24107947f8d0d88d3831e84f366\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"next": null,
"unread_total": 0,
"notifications": []
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": 1768927895224,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 62,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "d00334f0-917d-4e65-9fd7-44a1e0f57996",
"x-runtime": "0.032089",
"etag": "W/\"733a82e20cbe8c7635b4fd37017e97c1\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": [
{
"id": "a09f9a7e-afb7-489d-870d-e13a336e0b97",
"first_name": "Seth",
"last_name": "Call",
"name": "Seth Call",
"location": "Boston, MA",
"city": "Boston",
"state": "MA",
"country": "US",
"musician": true,
"email": "nuwan+6@jamkazam.com",
"online": false,
"photo_url": "https://s3.amazonaws.com/jamkazam-dev-public/avatars/a09f9a7e-afb7-489d-870d-e13a336e0b97/8EfyNy2cQPaxEsypRviW_IMG_20231224_133203_HDR.jpg"
}
]
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/history?includePending=false",
"pathname": "/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/history",
"timestamp": 1768927895224,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 208,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "aaa0508c-b4d8-4d24-8890-ec9266b1d4a8",
"x-runtime": "0.110980",
"etag": "W/\"1c90918fc0af71c877f432af9fddbff0\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"id": "c2682005-d64f-4f2e-9e72-b9017c0097fc",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-20T16:51:34.911Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "3b57a648-3858-44a0-8650-45555a6dd33b"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/ZAPOFY0FCPM",
"genres": [
"A Cappella"
],
"scheduled_start": "Tue 20 January 2026 22:21:00",
"pretty_scheduled_start_with_timezone": "Tuesday, January 20, 10:21-10:21 PM US Central Time",
"pretty_scheduled_start_short": "Tuesday, January 20 - 10:21pm"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/history?includePending=false",
"pathname": "/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/history",
"timestamp": 1768927895444,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 97,
"responseStatus": 200,
"responseHeaders": {
"x-runtime": "0.096440",
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"etag": "W/\"1c90918fc0af71c877f432af9fddbff0\"",
"vary": "Accept-Encoding, Origin",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "1; mode=block",
"x-request-id": "2c056ad2-8e49-4443-b0e2-04b5836dce4c"
},
"responseBody": {
"id": "c2682005-d64f-4f2e-9e72-b9017c0097fc",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-20T16:51:34.911Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "3b57a648-3858-44a0-8650-45555a6dd33b"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/ZAPOFY0FCPM",
"genres": [
"A Cappella"
],
"scheduled_start": "Tue 20 January 2026 22:21:00",
"pretty_scheduled_start_with_timezone": "Tuesday, January 20, 10:21-10:21 PM US Central Time",
"pretty_scheduled_start_short": "Tuesday, January 20 - 10:21pm"
}
},
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/participants",
"pathname": "/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/participants",
"timestamp": 1768927895579,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"requestBody": {
"client_id": "8d64a3b0-2f34-48c1-8199-74766ad6b1ac",
"as_musician": true,
"tracks": [
{
"client_track_id": "d4734735-0acb-4e87-9737-320efb024b3f",
"client_resource_id": "0x60000117dab0",
"instrument_id": "piano",
"sound": "stereo"
}
],
"client_role": "parent",
"parent_client_id": ""
},
"duration": 444,
"responseStatus": 201,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "74ae651d-b619-436b-bf23-0a101b498f10",
"x-runtime": "0.440659",
"etag": "W/\"511bac56d5816bf59cf165f3ab9f9289\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"location": "http://www.jamkazam.local:3000/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"id": "c2682005-d64f-4f2e-9e72-b9017c0097fc",
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"friends_can_join": true,
"fan_access": false,
"fan_chat": false,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"claimed_recording_initiator_id": null,
"track_changes_counter": 0,
"max_score": 0,
"backing_track_path": null,
"metronome_active": false,
"jam_track_initiator_id": null,
"jam_track_id": null,
"music_session_id_int": 3065,
"use_video_conferencing_server": true,
"created_at": "2026-01-20T16:51:35.602Z",
"music_notations": [],
"participants": [
{
"ip_address": "127.0.0.1",
"client_id": "8d64a3b0-2f34-48c1-8199-74766ad6b1ac",
"joined_session_at": "2026-01-20T16:51:35.654Z",
"id": "2a4bf89d-6da3-4e80-b253-deb796ebb489",
"metronome_open": false,
"is_jamblaster": false,
"client_role": "parent",
"parent_client_id": "",
"client_id_int": 93868,
"tracks": [
{
"id": "662bdeb9-3dfe-4753-98e4-3902b5a10534",
"connection_id": "2a4bf89d-6da3-4e80-b253-deb796ebb489",
"instrument_id": "piano",
"sound": "stereo",
"client_track_id": "d4734735-0acb-4e87-9737-320efb024b3f",
"client_resource_id": "0x60000117dab0",
"updated_at": "2026-01-20T16:51:35.657Z",
"instrument": "Piano"
}
],
"backing_tracks": [],
"user": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"name": "Nuwan Chaturanga",
"is_friend": false,
"connection_state": "connected",
"subscription": "jamsubplatinum"
}
}
],
"invitations": [],
"lesson_session": null,
"join_requests": [],
"jam_track": null,
"claimed_recording": null,
"subscription": {
"play_time_per_month": null,
"play_time_per_session": null,
"can_record_audio": true,
"can_record_video": true,
"can_use_video": true,
"can_record_wave": true,
"video_resolution": 4,
"audio_max_bitrate": 5,
"can_broadcast": true,
"broadcasting_type": 3,
"max_players": null,
"pro_audio": true,
"has_support": true,
"name": "Platinum",
"rank": 3,
"remaining_month_play_time": null
},
"session_rules": {
"remaining_session_play_time": null
},
"can_join": true,
"genres": [
"A Cappella"
],
"recording": null,
"share_url": "http://www.jamkazam.local:3000/s/ZAPOFY0FCPM",
"session_controller_id": "27bd4a30-d1b8-4eea-8454-01a104d59381"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc",
"pathname": "/api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc",
"timestamp": 1768927896184,
"requestHeaders": {
"accept": "application/json",
"referer": "http://beta.jamkazam.local:4000/",
"accept-language": "en-US",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"content-type": "application/json"
},
"duration": 182,
"responseStatus": 200,
"responseHeaders": {
"content-encoding": "gzip",
"x-content-type-options": "nosniff",
"transfer-encoding": "chunked",
"x-xss-protection": "1; mode=block",
"x-request-id": "320d63a4-cd6e-45c2-affb-dc16a4908dd8",
"x-runtime": "0.075026",
"etag": "W/\"1140a03eccac397577d88aa4add338ae\"",
"x-frame-options": "SAMEORIGIN",
"access-control-max-age": "7200",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"access-control-expose-headers": "",
"vary": "Accept-Encoding, Origin",
"access-control-allow-credentials": "true",
"content-type": "application/json; charset=utf-8",
"cache-control": "max-age=0, private, must-revalidate"
},
"responseBody": {
"id": "c2682005-d64f-4f2e-9e72-b9017c0097fc",
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"friends_can_join": true,
"fan_access": false,
"fan_chat": false,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"claimed_recording_initiator_id": null,
"track_changes_counter": 0,
"max_score": 0,
"backing_track_path": null,
"metronome_active": false,
"jam_track_initiator_id": null,
"jam_track_id": null,
"music_session_id_int": 3065,
"use_video_conferencing_server": true,
"created_at": "2026-01-20T16:51:35.602Z",
"music_notations": [],
"participants": [
{
"ip_address": "127.0.0.1",
"client_id": "8d64a3b0-2f34-48c1-8199-74766ad6b1ac",
"joined_session_at": "2026-01-20T16:51:35.654Z",
"id": "2a4bf89d-6da3-4e80-b253-deb796ebb489",
"metronome_open": false,
"is_jamblaster": false,
"client_role": "parent",
"parent_client_id": "",
"client_id_int": 93868,
"tracks": [
{
"id": "662bdeb9-3dfe-4753-98e4-3902b5a10534",
"connection_id": "2a4bf89d-6da3-4e80-b253-deb796ebb489",
"instrument_id": "piano",
"sound": "stereo",
"client_track_id": "d4734735-0acb-4e87-9737-320efb024b3f",
"client_resource_id": "0x60000117dab0",
"updated_at": "2026-01-20T16:51:35.657Z",
"instrument": "Piano"
}
],
"backing_tracks": [],
"user": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"name": "Nuwan Chaturanga",
"is_friend": false,
"connection_state": "connected",
"subscription": "jamsubplatinum"
}
}
],
"invitations": [],
"lesson_session": null,
"join_requests": [],
"jam_track": null,
"claimed_recording": null,
"can_join": true,
"genres": [
"A Cappella"
],
"recording": null,
"share_url": "http://www.jamkazam.local:3000/s/ZAPOFY0FCPM",
"session_controller_id": "27bd4a30-d1b8-4eea-8454-01a104d59381"
}
}
]

View File

@ -0,0 +1,199 @@
# API Sequence Comparison Report
## Summary
- **Match Status:** ❌ FAIL
- **Total Calls:** 9
- **Matched Calls:** 2
- **Match Percentage:** 22.2%
## ❌ Missing API Calls
The following expected API calls are missing or called fewer times:
- **GET /api/genres**
- Expected: 7 call(s)
- Actual: 0 call(s)
- Missing: 7 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381**
- Expected: 4 call(s)
- Actual: 0 call(s)
- Missing: 4 call(s)
- **GET /api/countries**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/shopping_carts**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/jamtracks/purchased**
- Expected: 5 call(s)
- Actual: 0 call(s)
- Missing: 5 call(s)
- **GET /api/teacher_distributions**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/regions**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **POST /xoplatform/logger/api/logger**
- Expected: 4 call(s)
- Actual: 0 call(s)
- Missing: 4 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/broadcast_notification**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/instruments**
- Expected: 6 call(s)
- Actual: 0 call(s)
- Missing: 6 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected: 4 call(s)
- Actual: 1 call(s)
- Missing: 3 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/notifications**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/languages**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/subjects**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/chat**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/versioncheck**
- Expected: 6 call(s)
- Actual: 0 call(s)
- Missing: 6 call(s)
- **GET /api/healthcheck**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **GET /api/config/client**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/sessions/scheduled**
- Expected: 2 call(s)
- Actual: 0 call(s)
- Missing: 2 call(s)
- **GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/history**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **POST /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/participants**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
- **PUT /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23/tracks**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **GET /api/sessions/02d4846c-eb02-40d5-ab2d-fe56e6c84d23**
- Expected: 3 call(s)
- Actual: 0 call(s)
- Missing: 3 call(s)
- **POST /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/udp_reachable**
- Expected: 1 call(s)
- Actual: 0 call(s)
- Missing: 1 call(s)
## ⚠️ Extra API Calls
The following API calls were made but not expected:
- **GET /api/app_features**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/me**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/history**
- Expected: 0 call(s)
- Actual: 2 call(s)
- Extra: 2 call(s)
- **POST /api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc/participants**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
- **GET /api/sessions/c2682005-d64f-4f2e-9e72-b9017c0097fc**
- Expected: 0 call(s)
- Actual: 1 call(s)
- Extra: 1 call(s)
## ⚠️ Out of Order Calls
The following API calls occurred in a different order:
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 15
- Actual position: 4
- Deviation: 11 positions
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 18
- Actual position: 4
- Deviation: 14 positions
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 39
- Actual position: 4
- Deviation: 35 positions
- **GET /api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends**
- Expected position: 48
- Actual position: 4
- Deviation: 44 positions
- **POST /api/sessions**
- Expected position: 57
- Actual position: 0
- Deviation: 57 positions
## ❌ Conclusion
The API sequence does NOT match the expected baseline. Please review the mismatches above.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View File

@ -0,0 +1,772 @@
[
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions",
"pathname": "/api/sessions",
"timestamp": 1768965359955,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"requestBody": {
"privacy": "1",
"description": "Automated test session",
"inviteeIds": "",
"musician_access": true,
"approval_required": false,
"legal_terms": true,
"start": "Wed Jan 21 2026 08:45 AM",
"duration": "60",
"invitations": [],
"timezone": "Central Time (US & Canada),America/Chicago",
"genres": [
"acapella"
],
"friends_can_join": true,
"is_unstructured_rsvp": false,
"fan_chat": false,
"fan_access": false,
"legal_policy": "legal_policy",
"language": "eng",
"name": "my session",
"rsvp_slots": [
{
"instrument_id": "other",
"proficiency_level": 3,
"approve": true
}
]
},
"duration": 137,
"responseStatus": 201,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "d243ef9d-ec9d-4609-a811-968f247214df",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"a25cbf7632128cc7cb369fd38d1ef3da\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.136211",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"id": "75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-21T03:15:59.980Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "64910edd-bd44-4540-87e7-2408fc55511f"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/73FTWXH9JE",
"genres": [
"A Cappella"
],
"scheduled_start": "Wed 21 January 2026 08:45:00",
"pretty_scheduled_start_with_timezone": "Wednesday, January 21, 8:45-8:45 AM US Central Time",
"pretty_scheduled_start_short": "Wednesday, January 21 - 8:45am"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/app_features?env=development",
"pathname": "/api/app_features",
"timestamp": 1768965360167,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 99,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "0ba2e442-a051-4194-9582-dcd7eb8809ee",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"73de1569c5c93f56982197c78242c09c\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.027957",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": []
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/me",
"pathname": "/api/me",
"timestamp": 1768965360167,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 112,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "190ed1a5-dd23-4947-973a-c94d06f5e5a6",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"6bfd801d7b90ab05f542b59add03f639\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.019786",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"email": "nuwan@jamkazam.com",
"photo_url": null,
"show_free_jamtrack": false,
"is_affiliate_partner": false,
"recording_pref": 1
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications?offset=0&limit=20",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications",
"timestamp": 1768965360280,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 4,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "40c18691-3e9b-44fa-ac7d-8ed7b2ac3ae6",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"a21896e7270a023e65a5c626034da71e\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.059304",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"next": null,
"unread_total": 0,
"notifications": []
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"pathname": "/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"timestamp": 1768965360282,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 28,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "0008ee3d-a1a9-47ee-8e75-e6d957bf8130",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"8ce855cfe7ebadc4676419d3f557926a\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.025263",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": [
{
"id": "a09f9a7e-afb7-489d-870d-e13a336e0b97",
"first_name": "Seth",
"last_name": "Call",
"name": "Seth Call",
"location": "Boston, MA",
"city": "Boston",
"state": "MA",
"country": "US",
"musician": true,
"email": "nuwan+6@jamkazam.com",
"online": false,
"photo_url": "https://s3.amazonaws.com/jamkazam-dev-public/avatars/a09f9a7e-afb7-489d-870d-e13a336e0b97/8EfyNy2cQPaxEsypRviW_IMG_20231224_133203_HDR.jpg"
}
]
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history?includePending=false",
"pathname": "/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history",
"timestamp": 1768965360282,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 202,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "5a4486be-1bcf-4521-9faa-fa0be99ae3d3",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"a25cbf7632128cc7cb369fd38d1ef3da\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.171552",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"id": "75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-21T03:15:59.980Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "64910edd-bd44-4540-87e7-2408fc55511f"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/73FTWXH9JE",
"genres": [
"A Cappella"
],
"scheduled_start": "Wed 21 January 2026 08:45:00",
"pretty_scheduled_start_with_timezone": "Wednesday, January 21, 8:45-8:45 AM US Central Time",
"pretty_scheduled_start_short": "Wednesday, January 21 - 8:45am"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history?includePending=false",
"pathname": "/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history",
"timestamp": 1768965360585,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 90,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "d563df44-1301-4a01-81a6-2e2641dc1b45",
"access-control-expose-headers": "",
"cache-control": "max-age=0, private, must-revalidate",
"content-encoding": "gzip",
"etag": "W/\"a25cbf7632128cc7cb369fd38d1ef3da\"",
"access-control-allow-credentials": "true",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"x-content-type-options": "nosniff",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.084096",
"x-frame-options": "SAMEORIGIN"
},
"responseBody": {
"id": "75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"music_session_id": null,
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"fan_access": false,
"fan_chat": false,
"create_type": null,
"band_id": null,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"genre_id": "acapella",
"created_at": "2026-01-21T03:15:59.980Z",
"like_count": 0,
"comment_count": 0,
"play_count": 0,
"scheduled_duration": "01:00:00",
"language": "eng",
"recurring_mode": "once",
"language_description": "English",
"scheduled_start_date": "Wed 21 January 2026",
"access_description": "Musicians may join at will. Fans may not listen to session.",
"timezone": "Central Time (US & Canada),America/Chicago",
"timezone_id": "America/Chicago",
"timezone_description": "Central Time (US & Canada)",
"musician_access_description": "Musicians may join at will",
"fan_access_description": "Fans may not listen to session",
"session_removed_at": null,
"legal_policy": "legal_policy",
"open_rsvps": false,
"is_unstructured_rsvp?": false,
"friends_can_join": true,
"use_video_conferencing_server": true,
"creator": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"name": "Nuwan Chaturanga",
"photo_url": null
},
"band": null,
"users": [],
"comments": [],
"session_info_comments": [],
"music_notations": [],
"invitations": [],
"approved_rsvps": [
{
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"first_name": "Nuwan",
"last_name": "Chaturanga",
"name": "Nuwan Chaturanga",
"resolved_photo_url": "http://localhost:3000/assets/shared/avatar_generic.png",
"full_score": null,
"audio_latency": 5,
"internet_score": null,
"instrument_list": [
{
"id": "other",
"desc": "Other",
"level": 3
}
],
"rsvp_request_id": "64910edd-bd44-4540-87e7-2408fc55511f"
}
],
"open_slots": [],
"pending_invitations": [],
"pending_rsvp_requests": [],
"lesson_session": null,
"active_music_session": null,
"can_join": true,
"share_url": "http://www.jamkazam.local:3000/s/73FTWXH9JE",
"genres": [
"A Cappella"
],
"scheduled_start": "Wed 21 January 2026 08:45:00",
"pretty_scheduled_start_with_timezone": "Wednesday, January 21, 8:45-8:45 AM US Central Time",
"pretty_scheduled_start_short": "Wednesday, January 21 - 8:45am"
}
},
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/participants",
"pathname": "/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/participants",
"timestamp": 1768965360692,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"requestBody": {
"client_id": "6cb293d8-1026-418f-9d4d-b240a3efa197",
"as_musician": true,
"tracks": [
{
"client_track_id": "b0cf76ae-0f87-4d0b-974c-75715b7bc9b6",
"client_resource_id": "0x600000e52300",
"instrument_id": "piano",
"sound": "stereo"
}
],
"client_role": "parent",
"parent_client_id": ""
},
"duration": 229,
"responseStatus": 201,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "126b756e-3b96-491d-abc6-e05a0fc780fa",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"2c5540ef347bb18a98bafdf3550f9f74\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.221143",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"location": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"id": "75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"friends_can_join": true,
"fan_access": false,
"fan_chat": false,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"claimed_recording_initiator_id": null,
"track_changes_counter": 0,
"max_score": 0,
"backing_track_path": null,
"metronome_active": false,
"jam_track_initiator_id": null,
"jam_track_id": null,
"music_session_id_int": 3070,
"use_video_conferencing_server": true,
"created_at": "2026-01-21T03:16:00.744Z",
"music_notations": [],
"participants": [
{
"ip_address": "127.0.0.1",
"client_id": "6cb293d8-1026-418f-9d4d-b240a3efa197",
"joined_session_at": "2026-01-21T03:16:00.776Z",
"id": "95ee4b0c-d824-4437-8bac-3c2387612c80",
"metronome_open": false,
"is_jamblaster": false,
"client_role": "parent",
"parent_client_id": "",
"client_id_int": 93886,
"tracks": [
{
"id": "18302dc0-49d8-4f9f-a035-86bb76029e7b",
"connection_id": "95ee4b0c-d824-4437-8bac-3c2387612c80",
"instrument_id": "piano",
"sound": "stereo",
"client_track_id": "b0cf76ae-0f87-4d0b-974c-75715b7bc9b6",
"client_resource_id": "0x600000e52300",
"updated_at": "2026-01-21T03:16:00.778Z",
"instrument": "Piano"
}
],
"backing_tracks": [],
"user": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"name": "Nuwan Chaturanga",
"is_friend": false,
"connection_state": "connected",
"subscription": "jamsubplatinum"
}
}
],
"invitations": [],
"lesson_session": null,
"join_requests": [],
"jam_track": null,
"claimed_recording": null,
"subscription": {
"play_time_per_month": null,
"play_time_per_session": null,
"can_record_audio": true,
"can_record_video": true,
"can_use_video": true,
"can_record_wave": true,
"video_resolution": 4,
"audio_max_bitrate": 5,
"can_broadcast": true,
"broadcasting_type": 3,
"max_players": null,
"pro_audio": true,
"has_support": true,
"name": "Platinum",
"rank": 3,
"remaining_month_play_time": null
},
"session_rules": {
"remaining_session_play_time": null
},
"can_join": true,
"genres": [
"A Cappella"
],
"recording": null,
"share_url": "http://www.jamkazam.local:3000/s/73FTWXH9JE",
"session_controller_id": "27bd4a30-d1b8-4eea-8454-01a104d59381"
}
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"pathname": "/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"timestamp": 1768965361003,
"requestHeaders": {
"referer": "http://beta.jamkazam.local:4000/",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36",
"accept": "application/json",
"content-type": "application/json",
"accept-language": "en-US"
},
"duration": 41,
"responseStatus": 200,
"responseHeaders": {
"access-control-max-age": "7200",
"x-request-id": "566a5e0a-fd08-43f8-9a9f-93d67633324a",
"access-control-expose-headers": "",
"content-encoding": "gzip",
"etag": "W/\"104777bf9b6eaaaeac5f7a30d6b85c77\"",
"x-content-type-options": "nosniff",
"access-control-allow-methods": "GET, POST, PUT, DELETE, OPTIONS",
"content-type": "application/json; charset=utf-8",
"vary": "Accept-Encoding, Origin",
"x-runtime": "0.039334",
"x-frame-options": "SAMEORIGIN",
"transfer-encoding": "chunked",
"cache-control": "max-age=0, private, must-revalidate",
"access-control-allow-credentials": "true",
"access-control-allow-origin": "http://beta.jamkazam.local:4000",
"x-xss-protection": "1; mode=block"
},
"responseBody": {
"id": "75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"name": "my session",
"description": "Automated test session",
"musician_access": true,
"approval_required": false,
"friends_can_join": true,
"fan_access": false,
"fan_chat": false,
"user_id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"claimed_recording_initiator_id": null,
"track_changes_counter": 0,
"max_score": 0,
"backing_track_path": null,
"metronome_active": false,
"jam_track_initiator_id": null,
"jam_track_id": null,
"music_session_id_int": 3070,
"use_video_conferencing_server": true,
"created_at": "2026-01-21T03:16:00.744Z",
"music_notations": [],
"participants": [
{
"ip_address": "127.0.0.1",
"client_id": "6cb293d8-1026-418f-9d4d-b240a3efa197",
"joined_session_at": "2026-01-21T03:16:00.776Z",
"id": "95ee4b0c-d824-4437-8bac-3c2387612c80",
"metronome_open": false,
"is_jamblaster": false,
"client_role": "parent",
"parent_client_id": "",
"client_id_int": 93886,
"tracks": [
{
"id": "18302dc0-49d8-4f9f-a035-86bb76029e7b",
"connection_id": "95ee4b0c-d824-4437-8bac-3c2387612c80",
"instrument_id": "piano",
"sound": "stereo",
"client_track_id": "b0cf76ae-0f87-4d0b-974c-75715b7bc9b6",
"client_resource_id": "0x600000e52300",
"updated_at": "2026-01-21T03:16:00.778Z",
"instrument": "Piano"
}
],
"backing_tracks": [],
"user": {
"id": "27bd4a30-d1b8-4eea-8454-01a104d59381",
"photo_url": null,
"name": "Nuwan Chaturanga",
"is_friend": false,
"connection_state": "connected",
"subscription": "jamsubplatinum"
}
}
],
"invitations": [],
"lesson_session": null,
"join_requests": [],
"jam_track": null,
"claimed_recording": null,
"can_join": true,
"genres": [
"A Cappella"
],
"recording": null,
"share_url": "http://www.jamkazam.local:3000/s/73FTWXH9JE",
"session_controller_id": "27bd4a30-d1b8-4eea-8454-01a104d59381"
}
}
]

View File

@ -0,0 +1,78 @@
{
"timestamp": "2026-01-21T03:16:03.972Z",
"restApiCalls": {
"total": 9,
"sessionCreation": [
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions",
"status": 201
}
],
"addParticipant": [
{
"method": "POST",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/participants",
"status": 201
}
],
"getSessionDetails": [
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9",
"status": 200
}
],
"getSessionHistory": [
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history?includePending=false",
"status": 200
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/sessions/75dd4b54-87a7-4fdf-b92b-42f35f1baca9/history?includePending=false",
"status": 200
}
],
"updateTracks": [],
"sessionChat": [],
"udpReachability": [],
"other": [
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/app_features?env=development",
"status": 200
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/me",
"status": 200
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/my_notifications?offset=0&limit=20",
"status": 200
},
{
"method": "GET",
"url": "http://www.jamkazam.local:3000/api/users/27bd4a30-d1b8-4eea-8454-01a104d59381/friends",
"status": 200
}
]
},
"websocketConnections": [
{
"url": "ws://localhost:3060/",
"type": "native",
"messageCount": 373,
"status": "open"
},
{
"url": "ws://localhost:6767/?channel_id=3c27dd42-1dd1-4d2f-e9f3-7092f347b244&token=CKXGCTBxfdGG12oXACOEYA&client_type=client&client_id=6cb293d8-1026-418f-9d4d-b240a3efa197&machine=1b4d0ddbf9c1d7cd0a311465676d86a5892a36ea&os=MacOSX-M&product=JamClientModern&udp_reachable=false",
"type": "other",
"messageCount": 4,
"status": "open"
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,166 @@
import { Page, Request, Response } from '@playwright/test';
export interface APICall {
method: string;
url: string;
pathname: string;
timestamp: number;
requestHeaders?: Record<string, string>;
requestBody?: any;
responseStatus?: number;
responseHeaders?: Record<string, string>;
responseBody?: any;
duration?: number;
}
export class APIInterceptor {
private calls: APICall[] = [];
private pendingRequests: Map<string, APICall> = new Map();
/**
* Start intercepting API calls on the given page
*/
intercept(page: Page) {
page.on('request', (request: Request) => {
this.recordRequest(request);
});
page.on('response', async (response: Response) => {
await this.recordResponse(response);
});
}
/**
* Record an API request
*/
private recordRequest(request: Request) {
const url = request.url();
// Only capture API calls
if (!url.includes('/api/')) {
return;
}
const parsedUrl = new URL(url);
const call: APICall = {
method: request.method(),
url: url,
pathname: parsedUrl.pathname,
timestamp: Date.now(),
requestHeaders: request.headers(),
};
// Try to capture request body
try {
const postData = request.postData();
if (postData) {
try {
call.requestBody = JSON.parse(postData);
} catch {
call.requestBody = postData;
}
}
} catch {
// Ignore if we can't get post data
}
// Store as pending until we get the response
this.pendingRequests.set(url, call);
}
/**
* Record an API response and merge with request data
*/
private async recordResponse(response: Response) {
const url = response.url();
// Only capture API calls
if (!url.includes('/api/')) {
return;
}
const pendingCall = this.pendingRequests.get(url);
if (pendingCall) {
// Calculate duration
pendingCall.duration = Date.now() - pendingCall.timestamp;
pendingCall.responseStatus = response.status();
pendingCall.responseHeaders = response.headers();
// Try to capture response body
try {
const contentType = response.headers()['content-type'] || '';
if (contentType.includes('application/json')) {
pendingCall.responseBody = await response.json();
}
} catch {
// Ignore if we can't parse response
}
this.calls.push(pendingCall);
this.pendingRequests.delete(url);
} else {
// Response without matching request - create minimal record
const parsedUrl = new URL(url);
this.calls.push({
method: 'UNKNOWN',
url: url,
pathname: parsedUrl.pathname,
timestamp: Date.now(),
responseStatus: response.status(),
responseHeaders: response.headers(),
});
}
}
/**
* Get all captured API calls
*/
getCalls(): APICall[] {
return this.calls;
}
/**
* Get calls filtered by method
*/
getCallsByMethod(method: string): APICall[] {
return this.calls.filter(call => call.method === method);
}
/**
* Get calls filtered by pathname pattern
*/
getCallsByPath(pattern: string | RegExp): APICall[] {
if (typeof pattern === 'string') {
return this.calls.filter(call => call.pathname.includes(pattern));
} else {
return this.calls.filter(call => pattern.test(call.pathname));
}
}
/**
* Get unique endpoints called
*/
getUniqueEndpoints(): string[] {
const endpoints = new Set<string>();
for (const call of this.calls) {
endpoints.add(`${call.method} ${call.pathname}`);
}
return Array.from(endpoints);
}
/**
* Reset the interceptor
*/
reset() {
this.calls = [];
this.pendingRequests.clear();
}
/**
* Export calls to JSON
*/
toJSON(): string {
return JSON.stringify(this.calls, null, 2);
}
}

View File

@ -0,0 +1,293 @@
import { APICall } from './api-interceptor';
export interface ComparisonResult {
matches: boolean;
totalCalls: number;
matchedCalls: number;
missingCalls: EndpointMismatch[];
extraCalls: EndpointMismatch[];
outOfOrderCalls: OrderMismatch[];
timingVariances: TimingVariance[];
report: string;
}
export interface EndpointMismatch {
endpoint: string;
expectedCount: number;
actualCount: number;
}
export interface OrderMismatch {
endpoint: string;
expectedPosition: number;
actualPosition: number;
deviation: number;
}
export interface TimingVariance {
endpoint: string;
expectedTiming: number;
actualTiming: number;
variance: number;
}
/**
* Compare two API call sequences
*/
export function compareAPISequences(
actual: APICall[],
expected: APICall[]
): ComparisonResult {
const missing: EndpointMismatch[] = [];
const extra: EndpointMismatch[] = [];
const outOfOrder: OrderMismatch[] = [];
const timingVariances: TimingVariance[] = [];
// Build endpoint maps
const actualEndpoints = buildEndpointMap(actual);
const expectedEndpoints = buildEndpointMap(expected);
// Find missing calls (in expected but not in actual)
for (const [endpoint, expectedCalls] of expectedEndpoints.entries()) {
const actualCalls = actualEndpoints.get(endpoint) || [];
if (actualCalls.length === 0) {
missing.push({
endpoint,
expectedCount: expectedCalls.length,
actualCount: 0,
});
} else if (actualCalls.length < expectedCalls.length) {
missing.push({
endpoint,
expectedCount: expectedCalls.length,
actualCount: actualCalls.length,
});
}
}
// Find extra calls (in actual but not in expected)
for (const [endpoint, actualCalls] of actualEndpoints.entries()) {
const expectedCalls = expectedEndpoints.get(endpoint) || [];
if (expectedCalls.length === 0) {
extra.push({
endpoint,
expectedCount: 0,
actualCount: actualCalls.length,
});
} else if (actualCalls.length > expectedCalls.length) {
extra.push({
endpoint,
expectedCount: expectedCalls.length,
actualCount: actualCalls.length,
});
}
}
// Check order
const orderMismatches = checkOrder(actual, expected);
outOfOrder.push(...orderMismatches);
// Check timing
const timings = checkTimingVariance(actual, expected);
timingVariances.push(...timings);
// Calculate match percentage
const totalExpected = expected.length;
const matched = totalExpected - missing.reduce((sum, m) => sum + (m.expectedCount - m.actualCount), 0);
const matchPercentage = totalExpected > 0 ? (matched / totalExpected) * 100 : 0;
// Generate report
const report = generateReport({
matches: matchPercentage >= 95,
totalCalls: actual.length,
matchedCalls: matched,
missingCalls: missing,
extraCalls: extra,
outOfOrderCalls: outOfOrder,
timingVariances: timingVariances,
report: '',
});
return {
matches: matchPercentage >= 95,
totalCalls: actual.length,
matchedCalls: matched,
missingCalls: missing,
extraCalls: extra,
outOfOrderCalls: outOfOrder,
timingVariances: timingVariances,
report,
};
}
/**
* Build a map of endpoint -> calls
*/
function buildEndpointMap(calls: APICall[]): Map<string, APICall[]> {
const map = new Map<string, APICall[]>();
for (const call of calls) {
const endpoint = `${call.method} ${call.pathname}`;
if (!map.has(endpoint)) {
map.set(endpoint, []);
}
map.get(endpoint)!.push(call);
}
return map;
}
/**
* Check if calls are in the same order
*/
function checkOrder(actual: APICall[], expected: APICall[]): OrderMismatch[] {
const mismatches: OrderMismatch[] = [];
// Create a sequence of endpoints for both
const actualSequence = actual.map(c => `${c.method} ${c.pathname}`);
const expectedSequence = expected.map(c => `${c.method} ${c.pathname}`);
// For each expected endpoint, find its position in actual
for (let i = 0; i < expectedSequence.length; i++) {
const endpoint = expectedSequence[i];
const actualIndex = actualSequence.indexOf(endpoint);
if (actualIndex !== -1) {
const deviation = Math.abs(actualIndex - i);
// Only report if deviation is significant (more than 3 positions)
if (deviation > 3) {
mismatches.push({
endpoint,
expectedPosition: i,
actualPosition: actualIndex,
deviation,
});
}
}
}
return mismatches;
}
/**
* Check timing variance between calls
*/
function checkTimingVariance(actual: APICall[], expected: APICall[]): TimingVariance[] {
const variances: TimingVariance[] = [];
if (actual.length === 0 || expected.length === 0) {
return variances;
}
// Calculate relative timings (time from first call)
const actualStartTime = actual[0].timestamp;
const expectedStartTime = expected[0].timestamp;
const actualTimings = new Map<string, number[]>();
const expectedTimings = new Map<string, number[]>();
for (const call of actual) {
const endpoint = `${call.method} ${call.pathname}`;
const relativeTime = call.timestamp - actualStartTime;
if (!actualTimings.has(endpoint)) {
actualTimings.set(endpoint, []);
}
actualTimings.get(endpoint)!.push(relativeTime);
}
for (const call of expected) {
const endpoint = `${call.method} ${call.pathname}`;
const relativeTime = call.timestamp - expectedStartTime;
if (!expectedTimings.has(endpoint)) {
expectedTimings.set(endpoint, []);
}
expectedTimings.get(endpoint)!.push(relativeTime);
}
// Compare average timings
for (const [endpoint, expectedTimes] of expectedTimings.entries()) {
const actualTimes = actualTimings.get(endpoint);
if (actualTimes && actualTimes.length > 0) {
const avgExpected = expectedTimes.reduce((a, b) => a + b, 0) / expectedTimes.length;
const avgActual = actualTimes.reduce((a, b) => a + b, 0) / actualTimes.length;
const variance = Math.abs(avgActual - avgExpected);
// Only report if variance is significant (more than 500ms)
if (variance > 500) {
variances.push({
endpoint,
expectedTiming: avgExpected,
actualTiming: avgActual,
variance,
});
}
}
}
return variances;
}
/**
* Generate a human-readable comparison report
*/
function generateReport(result: ComparisonResult): string {
let report = '# API Sequence Comparison Report\n\n';
report += `## Summary\n\n`;
report += `- **Match Status:** ${result.matches ? '✅ PASS' : '❌ FAIL'}\n`;
report += `- **Total Calls:** ${result.totalCalls}\n`;
report += `- **Matched Calls:** ${result.matchedCalls}\n`;
report += `- **Match Percentage:** ${((result.matchedCalls / result.totalCalls) * 100).toFixed(1)}%\n\n`;
if (result.missingCalls.length > 0) {
report += `## ❌ Missing API Calls\n\n`;
report += `The following expected API calls are missing or called fewer times:\n\n`;
for (const missing of result.missingCalls) {
report += `- **${missing.endpoint}**\n`;
report += ` - Expected: ${missing.expectedCount} call(s)\n`;
report += ` - Actual: ${missing.actualCount} call(s)\n`;
report += ` - Missing: ${missing.expectedCount - missing.actualCount} call(s)\n\n`;
}
}
if (result.extraCalls.length > 0) {
report += `## ⚠️ Extra API Calls\n\n`;
report += `The following API calls were made but not expected:\n\n`;
for (const extra of result.extraCalls) {
report += `- **${extra.endpoint}**\n`;
report += ` - Expected: ${extra.expectedCount} call(s)\n`;
report += ` - Actual: ${extra.actualCount} call(s)\n`;
report += ` - Extra: ${extra.actualCount - extra.expectedCount} call(s)\n\n`;
}
}
if (result.outOfOrderCalls.length > 0) {
report += `## ⚠️ Out of Order Calls\n\n`;
report += `The following API calls occurred in a different order:\n\n`;
for (const order of result.outOfOrderCalls) {
report += `- **${order.endpoint}**\n`;
report += ` - Expected position: ${order.expectedPosition}\n`;
report += ` - Actual position: ${order.actualPosition}\n`;
report += ` - Deviation: ${order.deviation} positions\n\n`;
}
}
if (result.timingVariances.length > 0) {
report += `## ⏱️ Timing Variances\n\n`;
report += `The following API calls have significant timing differences:\n\n`;
for (const timing of result.timingVariances) {
report += `- **${timing.endpoint}**\n`;
report += ` - Expected timing: ${timing.expectedTiming.toFixed(0)}ms\n`;
report += ` - Actual timing: ${timing.actualTiming.toFixed(0)}ms\n`;
report += ` - Variance: ${timing.variance.toFixed(0)}ms\n\n`;
}
}
if (result.matches) {
report += `## ✅ Conclusion\n\n`;
report += `The API sequence matches the expected baseline with acceptable variance.\n`;
} else {
report += `## ❌ Conclusion\n\n`;
report += `The API sequence does NOT match the expected baseline. Please review the mismatches above.\n`;
}
return report;
}

View File

@ -0,0 +1,387 @@
import { Page, expect } from '@playwright/test';
export interface LoginCredentials {
email: string;
password: string;
}
export interface SessionFormData {
sessionName?: string;
sessionType?: 'private' | 'public' | 'friends';
description?: string;
}
/**
* Login to jam-ui application
*/
export async function loginToJamUI(
page: Page,
credentials: LoginCredentials = {
email: 'nuwan@jamkazam.com',
password: 'jam123',
}
): Promise<void> {
await page.goto('http://beta.jamkazam.local:4000/');
// Wait for login form to be visible
await page.waitForSelector('input[name="email"], input[type="email"], #email', {
timeout: 10000,
});
// Fill in credentials - try multiple selectors
const emailSelectors = ['input[name="email"]', 'input[type="email"]', '#email', '[placeholder*="email" i]'];
for (const selector of emailSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.fill(selector, credentials.email);
break;
}
} catch {
continue;
}
}
const passwordSelectors = ['input[name="password"]', 'input[type="password"]', '#password'];
for (const selector of passwordSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.fill(selector, credentials.password);
break;
}
} catch {
continue;
}
}
// Submit form
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:has-text("Sign In")',
'button:has-text("Login")',
];
for (const selector of submitSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.click(selector);
break;
}
} catch {
continue;
}
}
// Wait for navigation after login
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(2000);
}
/**
* Navigate to session creation page
*/
export async function navigateToSessionCreation(page: Page): Promise<void> {
// Try multiple selectors for the create session link
const createSessionSelectors = [
'text=create session',
'text=Create Session',
'a:has-text("create session")',
'a:has-text("Create Session")',
'[href*="session"][href*="create"]',
'[data-testid="create-session"]',
];
let clicked = false;
for (const selector of createSessionSelectors) {
try {
const element = await page.$(selector);
if (element) {
await page.click(selector);
clicked = true;
break;
}
} catch {
continue;
}
}
if (!clicked) {
throw new Error('Could not find "Create Session" link');
}
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(1000);
}
/**
* Fill and submit session creation form (jam-ui uses form-based creation)
*/
export async function fillSessionForm(page: Page, formData: SessionFormData = {}): Promise<void> {
const {
sessionName = 'Test Session ' + Date.now(),
sessionType = 'private',
description = 'Automated test session',
} = formData;
console.log('Filling session creation form...');
// Wait for form to be visible
await page.waitForTimeout(1000);
// Fill session name - try multiple selectors
const nameSelectors = [
'input[name="session_name"]',
'input[name="name"]',
'input[name="sessionName"]',
'#session_name',
'#name',
'#sessionName',
'input[placeholder*="name" i]',
];
let nameFilled = false;
for (const selector of nameSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await page.fill(selector, sessionName);
console.log(` Filled session name using: ${selector}`);
nameFilled = true;
break;
}
} catch {
continue;
}
}
if (!nameFilled) {
console.log(' Warning: Could not find session name field');
}
// Select session type if there's a dropdown
const typeSelectors = [
'select[name="session_type"]',
'select[name="type"]',
'select[name="sessionType"]',
'#session_type',
'#type',
'#sessionType',
];
for (const selector of typeSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await page.selectOption(selector, sessionType);
console.log(` Selected session type: ${sessionType}`);
break;
}
} catch {
continue;
}
}
// Try clicking radio buttons for session type
const radioSelectors = [
`input[type="radio"][value="${sessionType}"]`,
`input[type="radio"][id*="${sessionType}" i]`,
`label:has-text("${sessionType}") input[type="radio"]`,
];
for (const selector of radioSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await page.click(selector);
console.log(` Clicked radio for: ${sessionType}`);
break;
}
} catch {
continue;
}
}
// Fill description if field exists
const descSelectors = [
'textarea[name="description"]',
'input[name="description"]',
'textarea[name="notes"]',
'#description',
'#notes',
];
for (const selector of descSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await page.fill(selector, description);
console.log(` Filled description`);
break;
}
} catch {
continue;
}
}
await page.waitForTimeout(500);
// Submit form
console.log('Submitting form...');
const submitSelectors = [
'button[type="submit"]',
'input[type="submit"]',
'button:has-text("Create Session")',
'button:has-text("Create")',
'button:has-text("Submit")',
'button:has-text("Start Session")',
];
let submitted = false;
for (const selector of submitSelectors) {
try {
const element = await page.$(selector);
if (element && await element.isVisible()) {
await page.click(selector);
console.log(` Clicked submit button: ${selector}`);
submitted = true;
break;
}
} catch {
continue;
}
}
if (!submitted) {
throw new Error('Could not find submit button for session creation form');
}
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
console.log('Form submitted, waiting for session to load...');
}
/**
* Create and join session using jam-ui's form-based approach
* (jam-ui doesn't have Quick Start buttons - it uses a form)
*/
export async function createAndJoinSession(
page: Page,
sessionData: SessionFormData = {}
): Promise<void> {
console.log('\nCreating session using jam-ui form...');
// Navigate to Create Session if not already there
const currentUrl = page.url();
if (!currentUrl.includes('session') || !currentUrl.includes('create')) {
console.log('Navigating to Create Session page...');
await navigateToSessionCreation(page);
}
// Fill and submit the session creation form
await fillSessionForm(page, {
sessionType: 'private',
...sessionData,
});
console.log('Session creation complete, should be in session interface now');
}
/**
* Legacy function for compatibility - jam-ui uses form-based creation
* @deprecated Use createAndJoinSession instead
*/
export async function clickQuickStart(page: Page, type: 'private' | 'public' | 'friends' = 'private'): Promise<void> {
console.log('Note: jam-ui uses form-based session creation, not Quick Start buttons');
await createAndJoinSession(page, { sessionType: type });
}
/**
* Verify session interface is loaded
*/
export async function verifySessionInterfaceLoaded(page: Page): Promise<void> {
// Check for key session interface elements
const requiredElements = [
{ name: 'audio inputs', selectors: ['text=audio inputs', 'text=Audio Inputs', '[data-testid="audio-inputs"]'] },
{ name: 'personal mix', selectors: ['text=personal mix', 'text=Personal Mix', '[data-testid="personal-mix"]'] },
{ name: 'LEAVE button', selectors: ['text=LEAVE', 'button:has-text("LEAVE")', '[data-testid="leave-button"]'] },
];
for (const element of requiredElements) {
let found = false;
for (const selector of element.selectors) {
try {
const el = await page.$(selector);
if (el) {
found = true;
break;
}
} catch {
continue;
}
}
if (!found) {
console.warn(`Expected element not found: ${element.name}`);
}
}
}
/**
* Wait for API calls to settle
*/
export async function waitForAPICalls(page: Page, timeout: number = 3000): Promise<void> {
await page.waitForLoadState('networkidle', { timeout }).catch(() => {});
await page.waitForTimeout(1000);
}
/**
* Dismiss any modals or overlays
*/
export async function dismissModals(page: Page): Promise<void> {
// Try pressing Escape
await page.keyboard.press('Escape');
await page.waitForTimeout(500);
// Try to remove dialog overlays
await page.evaluate(() => {
const overlays = document.querySelectorAll('.dialog-overlay, .modal-overlay, [role="dialog"]');
overlays.forEach(overlay => {
(overlay as HTMLElement).remove();
});
});
await page.waitForTimeout(500);
}
/**
* Extract session ID from current URL or page content
*/
export async function extractSessionId(page: Page): Promise<string | null> {
// Try to extract from URL
const url = page.url();
const urlMatch = url.match(/session[s]?\/([a-f0-9-]{36})/i);
if (urlMatch) {
return urlMatch[1];
}
// Try to extract from page content
const sessionId = await page.evaluate(() => {
// Look for session ID in data attributes
const sessionEl = document.querySelector('[data-session-id]');
if (sessionEl) {
return sessionEl.getAttribute('data-session-id');
}
// Look for session ID in window object
if ((window as any).sessionId) {
return (window as any).sessionId;
}
return null;
});
return sessionId;
}

View File

@ -0,0 +1,179 @@
import { Page, WebSocket } from '@playwright/test';
export interface WSConnection {
url: string;
timestamp: number;
isNativeClient: boolean;
isServerConnection: boolean;
messages: WSMessage[];
closed: boolean;
closedAt?: number;
}
export interface WSMessage {
direction: 'sent' | 'received';
payload: string;
timestamp: number;
size: number;
}
export class WebSocketMonitor {
private connections: WSConnection[] = [];
private activeConnections: Map<string, WSConnection> = new Map();
/**
* Start monitoring WebSocket connections on the given page
*/
monitor(page: Page) {
page.on('websocket', (ws: WebSocket) => {
this.recordConnection(ws);
});
}
/**
* Record a WebSocket connection and its messages
*/
private recordConnection(ws: WebSocket) {
const url = ws.url();
const timestamp = Date.now();
const connection: WSConnection = {
url: url,
timestamp: timestamp,
isNativeClient: url.includes('localhost:3060'),
isServerConnection: url.includes('jamkazam.local:6767') || url.includes('jamkazam.com:6767'),
messages: [],
closed: false,
};
console.log(`[WebSocket Monitor] Connection opened: ${url}`);
// Listen for sent messages
ws.on('framesent', frame => {
const payload = frame.payload.toString();
const message: WSMessage = {
direction: 'sent',
payload: payload,
timestamp: Date.now(),
size: payload.length,
};
connection.messages.push(message);
});
// Listen for received messages
ws.on('framereceived', frame => {
const payload = frame.payload.toString();
const message: WSMessage = {
direction: 'received',
payload: payload,
timestamp: Date.now(),
size: payload.length,
};
connection.messages.push(message);
});
// Listen for close
ws.on('close', () => {
connection.closed = true;
connection.closedAt = Date.now();
console.log(`[WebSocket Monitor] Connection closed: ${url}`);
});
this.connections.push(connection);
this.activeConnections.set(url, connection);
}
/**
* Get all connections
*/
getConnections(): WSConnection[] {
return this.connections;
}
/**
* Get active (non-closed) connections
*/
getActiveConnections(): WSConnection[] {
return this.connections.filter(conn => !conn.closed);
}
/**
* Get native client connection (ws://localhost:3060/)
*/
getNativeClientConnection(): WSConnection | undefined {
return this.connections.find(conn => conn.isNativeClient);
}
/**
* Get server connection (ws://jamkazam.local:6767/websocket)
*/
getServerConnection(): WSConnection | undefined {
return this.connections.find(conn => conn.isServerConnection);
}
/**
* Get connection count
*/
getConnectionCount(): number {
return this.connections.length;
}
/**
* Get total message count across all connections
*/
getTotalMessageCount(): number {
return this.connections.reduce((sum, conn) => sum + conn.messages.length, 0);
}
/**
* Get messages from a specific connection
*/
getMessagesFromConnection(url: string): WSMessage[] {
const conn = this.connections.find(c => c.url === url);
return conn ? conn.messages : [];
}
/**
* Verify dual WebSocket connections are established
*/
verifyDualConnections(): {
hasNativeClient: boolean;
hasServerConnection: boolean;
bothEstablished: boolean;
} {
const hasNativeClient = this.getNativeClientConnection() !== undefined;
const hasServerConnection = this.getServerConnection() !== undefined;
return {
hasNativeClient,
hasServerConnection,
bothEstablished: hasNativeClient && hasServerConnection,
};
}
/**
* Reset the monitor
*/
reset() {
this.connections = [];
this.activeConnections.clear();
}
/**
* Export connections to JSON
*/
toJSON(): string {
return JSON.stringify(
this.connections.map(conn => ({
...conn,
// Truncate messages for readability
messages: conn.messages.map(msg => ({
...msg,
payload: msg.payload.length > 200 ? msg.payload.substring(0, 200) + '...' : msg.payload,
})),
})),
null,
2
);
}
}

View File

@ -0,0 +1,497 @@
import { chromium } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
/**
* Detailed step-by-step verification of the session join flow
* This script goes through each step individually and captures API calls at each stage
*/
interface StepCapture {
stepNumber: number;
stepName: string;
apiCalls: any[];
wsMessages: any[];
screenshots: string[];
timestamp: string;
}
async function verifySessionFlow() {
console.log('='.repeat(80));
console.log('SESSION FLOW STEP-BY-STEP VERIFICATION');
console.log('='.repeat(80));
console.log();
const browser = await chromium.launch({
headless: false,
slowMo: 800,
});
const testResultsDir = path.join(__dirname, '../test-results/step-verification');
if (!fs.existsSync(testResultsDir)) {
fs.mkdirSync(testResultsDir, { recursive: true });
}
const context = await browser.newContext({
ignoreHTTPSErrors: true,
});
const page = await context.newPage();
const stepCaptures: StepCapture[] = [];
let currentStepApiCalls: any[] = [];
let currentStepWsMessages: any[] = [];
// Listen to all network activity
page.on('request', request => {
const url = request.url();
if (url.includes('/api/')) {
currentStepApiCalls.push({
type: 'request',
method: request.method(),
url: url,
timestamp: new Date().toISOString(),
});
console.log(` [API] ${request.method()} ${url}`);
}
});
page.on('response', async response => {
const url = response.url();
if (url.includes('/api/')) {
let body = null;
try {
const contentType = response.headers()['content-type'] || '';
if (contentType.includes('application/json')) {
body = await response.json();
}
} catch (e) {
// Ignore
}
currentStepApiCalls.push({
type: 'response',
status: response.status(),
url: url,
body: body,
timestamp: new Date().toISOString(),
});
}
});
page.on('websocket', ws => {
console.log(` [WebSocket] Connected: ${ws.url()}`);
currentStepWsMessages.push({
event: 'open',
url: ws.url(),
timestamp: new Date().toISOString(),
});
ws.on('framesent', frame => {
currentStepWsMessages.push({
event: 'sent',
payload: frame.payload.toString().substring(0, 200),
timestamp: new Date().toISOString(),
});
});
ws.on('framereceived', frame => {
currentStepWsMessages.push({
event: 'received',
payload: frame.payload.toString().substring(0, 200),
timestamp: new Date().toISOString(),
});
});
});
function saveStepCapture(stepNumber: number, stepName: string) {
const capture: StepCapture = {
stepNumber,
stepName,
apiCalls: [...currentStepApiCalls],
wsMessages: [...currentStepWsMessages],
screenshots: [],
timestamp: new Date().toISOString(),
};
stepCaptures.push(capture);
// Save individual step data
const stepDir = path.join(testResultsDir, `step-${stepNumber}`);
if (!fs.existsSync(stepDir)) {
fs.mkdirSync(stepDir, { recursive: true });
}
fs.writeFileSync(
path.join(stepDir, 'api-calls.json'),
JSON.stringify(currentStepApiCalls, null, 2)
);
fs.writeFileSync(
path.join(stepDir, 'ws-messages.json'),
JSON.stringify(currentStepWsMessages, null, 2)
);
console.log(` ✓ Captured ${currentStepApiCalls.length} API calls`);
console.log(` ✓ Captured ${currentStepWsMessages.length} WebSocket messages`);
// Reset for next step
currentStepApiCalls = [];
currentStepWsMessages = [];
}
try {
// STEP 1: User Authentication
console.log('\n' + '='.repeat(80));
console.log('STEP 1: USER AUTHENTICATION');
console.log('='.repeat(80));
console.log('Navigating to signin page...');
await page.goto('http://www.jamkazam.local:3100/signin', {
waitUntil: 'networkidle',
timeout: 30000,
});
await page.screenshot({
path: path.join(testResultsDir, 'step-1-signin-page.png'),
fullPage: true,
});
console.log('Filling in credentials...');
await page.fill('input#session_email', 'nuwan@jamkazam.com');
await page.fill('input#session_password', 'jam123');
console.log('Clicking sign in...');
await page.click('input[type="submit"]');
console.log('Waiting for login to complete...');
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
await page.waitForTimeout(3000);
await page.screenshot({
path: path.join(testResultsDir, 'step-1-after-login.png'),
fullPage: true,
});
saveStepCapture(1, 'User Authentication');
// STEP 2: Dashboard Load
console.log('\n' + '='.repeat(80));
console.log('STEP 2: DASHBOARD LOAD');
console.log('='.repeat(80));
console.log('Dashboard is loading (WebSocket connection should establish)...');
await page.waitForTimeout(5000); // Wait for all dashboard data to load
await page.screenshot({
path: path.join(testResultsDir, 'step-2-dashboard-loaded.png'),
fullPage: true,
});
saveStepCapture(2, 'Dashboard Load');
// STEP 3: Skip Upgrade Modal
console.log('\n' + '='.repeat(80));
console.log('STEP 3: SKIP UPGRADE MODAL');
console.log('='.repeat(80));
console.log('Pressing Cmd+Shift+0 to dismiss upgrade modal...');
await page.keyboard.press('Meta+Shift+Digit0');
await page.waitForTimeout(2000);
// Check if overlay still exists and try to dismiss it
const overlay = await page.$('.dialog-overlay');
if (overlay) {
console.log('Overlay still present, trying to click it or escape...');
await page.keyboard.press('Escape');
await page.waitForTimeout(1000);
// If still there, try clicking outside
const overlayStill = await page.$('.dialog-overlay');
if (overlayStill) {
console.log('Trying to force-remove overlay...');
await page.evaluate(() => {
const overlays = document.querySelectorAll('.dialog-overlay');
overlays.forEach(o => o.remove());
});
await page.waitForTimeout(500);
}
}
await page.screenshot({
path: path.join(testResultsDir, 'step-3-modal-dismissed.png'),
fullPage: true,
});
saveStepCapture(3, 'Skip Upgrade Modal');
// STEP 4: Navigate to Create Session
console.log('\n' + '='.repeat(80));
console.log('STEP 4: NAVIGATE TO CREATE SESSION');
console.log('='.repeat(80));
console.log('Looking for Create Session button...');
// Remove any remaining overlays
await page.evaluate(() => {
const overlays = document.querySelectorAll('.dialog-overlay');
overlays.forEach(o => o.remove());
});
await page.waitForTimeout(500);
// Try multiple selectors
const createSessionSelectors = [
'text=create session',
'h2:has-text("create session")',
'[data-testid="create-session"]',
'button:has-text("Create Session")',
'a:has-text("Create Session")',
'div:has-text("Create Session")',
'.tile:has-text("Create Session")',
'[title*="Create Session"]',
];
let sessionClicked = false;
for (const selector of createSessionSelectors) {
try {
const element = await page.$(selector);
if (element) {
console.log(`Found Create Session using: ${selector}`);
// Use force click to bypass any remaining overlays
await page.click(selector, { force: true });
sessionClicked = true;
break;
}
} catch (e) {
console.log(`Selector ${selector} failed: ${e.message}`);
continue;
}
}
if (!sessionClicked) {
console.warn('Could not find "Create Session" button - saving partial results...');
}
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(2000);
await page.screenshot({
path: path.join(testResultsDir, 'step-4-session-page.png'),
fullPage: true,
});
saveStepCapture(4, 'Navigate to Create Session');
// STEP 5: Create Quick Start Session
console.log('\n' + '='.repeat(80));
console.log('STEP 5: CREATE QUICK START SESSION');
console.log('='.repeat(80));
console.log('Pressing Ctrl+Shift+0 to enable native client features...');
await page.keyboard.press('Control+Shift+Digit0');
await page.waitForTimeout(2000);
await page.screenshot({
path: path.join(testResultsDir, 'step-5-before-quickstart.png'),
fullPage: true,
});
console.log('Looking for Quick Start button...');
const quickStartSelectors = [
'text=QUICK START PRIVATE',
'text=QUICK START PUBLIC',
'text=QUICK START FRIENDS',
'button:has-text("QUICK START")',
'button:has-text("Quick Start")',
'[data-testid="quick-start"]',
];
let quickStartClicked = false;
for (const selector of quickStartSelectors) {
try {
const element = await page.$(selector);
if (element) {
console.log(`Found Quick Start button using: ${selector}`);
await page.click(selector, { force: true });
quickStartClicked = true;
console.log('Quick Start button clicked!');
break;
}
} catch (e) {
console.log(`Selector ${selector} failed: ${e.message}`);
continue;
}
}
if (!quickStartClicked) {
console.warn('Could not find Quick Start button - trying generic button click');
// Try to click any button with "QUICK START" text
try {
await page.evaluate(() => {
const buttons = Array.from(document.querySelectorAll('button'));
const quickStartBtn = buttons.find(btn => btn.textContent?.includes('QUICK START'));
if (quickStartBtn) {
(quickStartBtn as HTMLElement).click();
return true;
}
return false;
});
console.log('Clicked Quick Start via evaluate');
quickStartClicked = true;
} catch (e) {
console.error('Failed to click Quick Start button');
}
}
console.log('Waiting for session to initialize...');
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
await page.waitForTimeout(5000); // Wait for WebSocket messages
await page.screenshot({
path: path.join(testResultsDir, 'step-5-session-joined.png'),
fullPage: true,
});
saveStepCapture(5, 'Create Quick Start Session');
// Generate summary report
console.log('\n' + '='.repeat(80));
console.log('GENERATING SUMMARY REPORT');
console.log('='.repeat(80));
const report = generateReport(stepCaptures);
fs.writeFileSync(path.join(testResultsDir, 'VERIFICATION_REPORT.md'), report);
console.log(`✓ Report saved to: ${testResultsDir}/VERIFICATION_REPORT.md`);
// Save complete capture
fs.writeFileSync(
path.join(testResultsDir, 'complete-capture.json'),
JSON.stringify(stepCaptures, null, 2)
);
console.log('\nKeeping browser open for 10 seconds for inspection...');
await page.waitForTimeout(10000);
} catch (error) {
console.error('\n❌ Error during verification:', error);
await page.screenshot({
path: path.join(testResultsDir, 'error-screenshot.png'),
fullPage: true,
});
throw error;
} finally {
await context.close();
await browser.close();
}
}
function generateReport(stepCaptures: StepCapture[]): string {
let report = `# Session Flow Verification Report
**Generated:** ${new Date().toLocaleString()}
## Summary
This report documents the exact API calls and WebSocket messages that occur at each step of the session join flow.
`;
for (const step of stepCaptures) {
const apiRequests = step.apiCalls.filter(c => c.type === 'request');
const uniqueEndpoints = new Set(
apiRequests.map(c => `${c.method} ${new URL(c.url).pathname}`)
);
report += `
## Step ${step.stepNumber}: ${step.stepName}
**Timestamp:** ${step.timestamp}
### API Calls Made (${apiRequests.length} requests)
`;
if (uniqueEndpoints.size > 0) {
report += 'Unique endpoints called:\n';
for (const endpoint of Array.from(uniqueEndpoints)) {
const count = apiRequests.filter(
c => `${c.method} ${new URL(c.url).pathname}` === endpoint
).length;
report += `- ${endpoint}${count > 1 ? ` (${count} times)` : ''}\n`;
}
} else {
report += 'No API calls made during this step.\n';
}
report += `
### WebSocket Activity
- Total messages sent/received: ${step.wsMessages.length}
- WebSocket connection events: ${step.wsMessages.filter(m => m.event === 'open').length}
`;
report += `
### Screenshots
- Available at: \`step-${step.stepNumber}/\`
---
`;
}
// Add comparison section
report += `
## Key Findings
### Step-by-Step API Call Summary
`;
for (const step of stepCaptures) {
const apiRequests = step.apiCalls.filter(c => c.type === 'request');
report += `**Step ${step.stepNumber} (${step.stepName}):** ${apiRequests.length} API calls\n`;
}
report += `
### WebSocket Message Summary
`;
for (const step of stepCaptures) {
report += `**Step ${step.stepNumber} (${step.stepName}):** ${step.wsMessages.length} messages\n`;
}
report += `
## Recommendations
Based on this verification:
1. Review the API calls at each step to ensure the migration plan accurately reflects them
2. Pay special attention to Step 5 (Create Quick Start Session) - this is where session creation happens
3. Verify WebSocket message sequences match between legacy and new implementation
4. Check if removing the separate "trick browser" step changed any API behavior
## Next Steps
1. Compare this capture with the original capture to identify any differences
2. Update the migration plan if any discrepancies are found
3. Use these findings to create more accurate Playwright tests
`;
return report;
}
// Run the verification
verifySessionFlow()
.then(() => {
console.log('\n✅ Verification completed successfully!');
process.exit(0);
})
.catch(error => {
console.error('\n❌ Verification failed:', error);
process.exit(1);
});

View File

@ -0,0 +1,174 @@
import { test, expect } from '@playwright/test';
import { WebSocketMonitor } from '../utils/websocket-monitor';
import { loginToJamUI, navigateToSessionCreation, fillSessionForm, waitForAPICalls } from '../utils/test-helpers';
import * as fs from 'fs';
import * as path from 'path';
test.describe('WebSocket Connection Verification', () => {
test('dual WebSocket connections are established during session join', async ({ page }) => {
const wsMonitor = new WebSocketMonitor();
wsMonitor.monitor(page);
// Login
await loginToJamUI(page);
await waitForAPICalls(page);
// Navigate to session creation
await navigateToSessionCreation(page);
await waitForAPICalls(page);
// Create session using form
await fillSessionForm(page);
await waitForAPICalls(page, 5000);
// Verify connections
const verification = wsMonitor.verifyDualConnections();
console.log('\nWebSocket Connection Verification:');
console.log(` Native client connection: ${verification.hasNativeClient ? 'YES' : 'NO'}`);
console.log(` Server connection: ${verification.hasServerConnection ? 'YES' : 'NO'}`);
console.log(` Both established: ${verification.bothEstablished ? 'YES' : 'NO'}`);
const connections = wsMonitor.getConnections();
console.log(`\nTotal connections: ${connections.length}`);
connections.forEach((conn, idx) => {
console.log(` ${idx + 1}. ${conn.url}`);
console.log(` Messages: ${conn.messages.length}`);
console.log(` Type: ${conn.isNativeClient ? 'Native Client' : conn.isServerConnection ? 'Server' : 'Unknown'}`);
});
// Save results
const resultsDir = path.join(__dirname, '../test-results/websocket-verification');
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true });
}
fs.writeFileSync(
path.join(resultsDir, 'connections.json'),
wsMonitor.toJSON()
);
// Assertions
expect(verification.bothEstablished).toBe(true);
expect(wsMonitor.getConnectionCount()).toBeGreaterThanOrEqual(2);
});
test('native client connection is to localhost:3060', async ({ page }) => {
const wsMonitor = new WebSocketMonitor();
wsMonitor.monitor(page);
await loginToJamUI(page);
await navigateToSessionCreation(page);
await page.keyboard.press('Control+Shift+Digit0');
await waitForAPICalls(page, 3000);
await clickQuickStart(page);
await waitForAPICalls(page, 5000);
const nativeClient = wsMonitor.getNativeClientConnection();
expect(nativeClient).toBeDefined();
expect(nativeClient?.url).toContain('localhost:3060');
expect(nativeClient?.url).toContain('ws://');
console.log(`\nNative client connection URL: ${nativeClient?.url}`);
console.log(`Messages sent/received: ${nativeClient?.messages.length || 0}`);
});
test('server connection is to jamkazam.local:6767', async ({ page }) => {
const wsMonitor = new WebSocketMonitor();
wsMonitor.monitor(page);
await loginToJamUI(page);
await navigateToSessionCreation(page);
await page.keyboard.press('Control+Shift+Digit0');
await waitForAPICalls(page, 3000);
await clickQuickStart(page);
await waitForAPICalls(page, 5000);
const serverConn = wsMonitor.getServerConnection();
expect(serverConn).toBeDefined();
expect(serverConn?.url).toMatch(/jamkazam\.(local|com):6767/);
expect(serverConn?.url).toContain('ws://');
expect(serverConn?.url).toContain('/websocket');
console.log(`\nServer connection URL: ${serverConn?.url}`);
console.log(`Messages sent/received: ${serverConn?.messages.length || 0}`);
// Check URL parameters
if (serverConn) {
const url = new URL(serverConn.url);
console.log('\nServer connection parameters:');
console.log(` channel_id: ${url.searchParams.get('channel_id') || 'N/A'}`);
console.log(` client_type: ${url.searchParams.get('client_type') || 'N/A'}`);
console.log(` client_id: ${url.searchParams.get('client_id') || 'N/A'}`);
console.log(` product: ${url.searchParams.get('product') || 'N/A'}`);
// Verify expected parameters
expect(url.searchParams.get('client_type')).toBe('browser');
expect(url.searchParams.get('product')).toBe('JamClientModern');
}
});
test('WebSocket connections send and receive messages', async ({ page }) => {
const wsMonitor = new WebSocketMonitor();
wsMonitor.monitor(page);
await loginToJamUI(page);
await navigateToSessionCreation(page);
await page.keyboard.press('Control+Shift+Digit0');
await waitForAPICalls(page, 3000);
await clickQuickStart(page);
await waitForAPICalls(page, 10000); // Wait longer for messages
const totalMessages = wsMonitor.getTotalMessageCount();
console.log(`\nTotal WebSocket messages: ${totalMessages}`);
const connections = wsMonitor.getConnections();
for (const conn of connections) {
const sentCount = conn.messages.filter(m => m.direction === 'sent').length;
const receivedCount = conn.messages.filter(m => m.direction === 'received').length;
console.log(`\n${conn.isNativeClient ? 'Native Client' : 'Server'} (${conn.url}):`);
console.log(` Sent: ${sentCount}`);
console.log(` Received: ${receivedCount}`);
console.log(` Total: ${conn.messages.length}`);
}
// Should have substantial message traffic
expect(totalMessages).toBeGreaterThan(100);
});
test('WebSocket connections remain active during session', async ({ page }) => {
const wsMonitor = new WebSocketMonitor();
wsMonitor.monitor(page);
await loginToJamUI(page);
await navigateToSessionCreation(page);
await page.keyboard.press('Control+Shift+Digit0');
await waitForAPICalls(page, 3000);
await clickQuickStart(page);
await waitForAPICalls(page, 5000);
// Wait a bit longer
await page.waitForTimeout(3000);
const activeConnections = wsMonitor.getActiveConnections();
console.log(`\nActive WebSocket connections: ${activeConnections.length}`);
activeConnections.forEach(conn => {
console.log(` ${conn.isNativeClient ? 'Native Client' : 'Server'}: OPEN`);
});
// Both connections should still be active
expect(activeConnections.length).toBeGreaterThanOrEqual(2);
// Verify neither connection is closed
const nativeClient = wsMonitor.getNativeClientConnection();
const server = wsMonitor.getServerConnection();
expect(nativeClient?.closed).toBe(false);
expect(server?.closed).toBe(false);
});
});