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:
parent
23594959f3
commit
b5f7f25698
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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'}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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`);
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
@ -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
6066
jam-ui/test/fixtures/legacy-sequences/step-5-session-creation-sequence.json
vendored
Normal file
6066
jam-ui/test/fixtures/legacy-sequences/step-5-session-creation-sequence.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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 |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue