feat: update metronome track layout and improve test infrastructure

- Change metronome track to horizontal layout: VU meter on left, volume slider on right
- Add three dots menu button without diagnostics circle (metronome doesn't need diagnostics)
- Move Close functionality to three dots menu dropdown
- Update test selectors to match new layout (.track-title, .track-menu-button)
- Add jamClient mocking in tests to prevent hanging on native client calls
- Increase test timeout to 60s to accommodate session creation time
- Configure global test setup to use existing user accounts instead of creating new ones
- Add conditional fake client support in JamClientContext for test environments

Tests: 3 passed, 2 skipped (as expected)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-25 13:59:29 +05:30
parent a2be53bc0d
commit 3887f4fdda
4 changed files with 134 additions and 61 deletions

View File

@ -3,8 +3,6 @@ import { useMixersContext } from '../../context/MixersContext';
import { useJamClient } from '../../context/JamClientContext';
import SessionTrackVU from './SessionTrackVU';
import SessionTrackGain from './SessionTrackGain';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import './JKSessionMyTrack.css';
const JKSessionMetronome = ({
@ -13,6 +11,7 @@ const JKSessionMetronome = ({
}) => {
const mixerHelper = useMixersContext();
const jamClient = useJamClient();
const [showMenu, setShowMenu] = React.useState(false);
console.log('JKSessionMetronome mixers:', mixers);
@ -27,8 +26,12 @@ const JKSessionMetronome = ({
return (
<div className={trackClasses}>
<div className="disabled-track-overlay" />
<div className="session-track-contents">
{/* Metronome Title */}
<div className="track-title" style={{ textAlign: 'center', marginBottom: '16px', fontSize: '0.9rem', fontWeight: '500', color: '#666' }}>
Metronome
</div>
{/* Metronome Icon */}
<div
className="track-instrument"
@ -36,14 +39,20 @@ const JKSessionMetronome = ({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginBottom: '8px'
marginBottom: '12px'
}}
>
<img
height="45"
height="55"
src="/assets/content/icon_metronome.png"
width="45"
width="55"
alt="metronome"
style={{
borderRadius: '50%',
padding: '8px',
backgroundColor: '#f5f5f5',
border: '2px solid #ddd'
}}
onError={(e) => {
// Fallback if metronome icon doesn't exist
e.target.src = "/assets/content/icon_recording.png";
@ -51,8 +60,14 @@ const JKSessionMetronome = ({
/>
</div>
{/* Controls */}
{/* 0db Label */}
<div style={{ textAlign: 'center', fontSize: '0.75rem', color: '#666', marginBottom: '8px' }}>
0db
</div>
{/* Track Controls */}
<div className="track-controls">
{/* VU Meter and Gain Control - Horizontal */}
<div className="vu-and-gain" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<SessionTrackVU
orientation="horizontal"
@ -70,37 +85,29 @@ const JKSessionMetronome = ({
/>
</div>
{/* Close Button */}
{/* Track Buttons - Three Dots Menu */}
<div className="track-buttons">
<div className="track-menu-container">
<button
className="btn-close-track"
onClick={handleClose}
title="Close Metronome"
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '4px 8px',
fontSize: '1.2rem',
color: '#6c757d',
transition: 'color 0.2s'
}}
onMouseEnter={(e) => e.target.style.color = '#495057'}
onMouseLeave={(e) => e.target.style.color = '#6c757d'}
>
<FontAwesomeIcon icon={faTimes} />
</button>
<div className="track-menu-button" onClick={() => setShowMenu(!showMenu)}>
...
</div>
{showMenu && (
<div className="track-menu">
<div
onClick={() => {
handleClose();
setShowMenu(false);
}}
>
Close
</div>
</div>
)}
</div>
</div>
<br className="clearall" />
</div>
{/* Track Info */}
<div className="track-info" style={{ marginTop: '8px', textAlign: 'center' }}>
<div className="track-name" style={{ fontSize: '0.875rem', fontWeight: '600', color: '#495057' }}>
Metronome
</div>
</div>
<br className="clearall" />
</div>
</div>
);

View File

@ -22,25 +22,25 @@ export const JamClientProvider = ({ children }) => {
const proxyRef = useRef(null);
// if (process.env.NODE_ENV === 'development') {
// const fakeJamClientMessages = new FakeJamClientMessages();
// const proxy = new FakeJamClientProxy(app, fakeJamClientMessages); // Pass appropriate parameters
// proxyRef.current = proxy.init();
// // For testing purposes, we can add some fake recordings
// const fakeJamClientRecordings = new FakeJamClientRecordings(app, proxyRef.current, fakeJamClientMessages);
// proxyRef.current.SetFakeRecordingImpl(fakeJamClientRecordings);
// } else {
// if (!proxyRef.current) {
// const proxy = new JamClientProxy(app, console, globalObject);
// proxyRef.current = proxy.init();
// }
// }
if (process.env.NODE_ENV === 'test' || process.env.REACT_APP_USE_FAKE_CLIENT === 'true') {
const fakeJamClientMessages = new FakeJamClientMessages();
const proxy = new FakeJamClientProxy(app, fakeJamClientMessages); // Pass appropriate parameters
proxyRef.current = proxy.init();
// For testing purposes, we can add some fake recordings
const fakeJamClientRecordings = new FakeJamClientRecordings(app, proxyRef.current, fakeJamClientMessages);
proxyRef.current.SetFakeRecordingImpl(fakeJamClientRecordings);
} else {
if (!proxyRef.current) {
const proxy = new JamClientProxy(app, console, globalObject);
proxyRef.current = proxy.init();
}
}
if (!proxyRef.current) {
const proxy = new JamClientProxy(app, console, globalObject);
proxyRef.current = proxy.init();
window.jamClient = proxyRef.current; // TODO: Expose jamClient globally for debugging. This is temporarily added. Remove it later.
}
window.jamClient = proxyRef.current; // TODO: Expose jamClient globally for debugging. This is temporarily added. Remove it later.
// Register metronome callback with jamClient when it's available
useEffect(() => {

View File

@ -23,19 +23,19 @@ async function globalSetup(config: FullConfig) {
const browser = await chromium.launch();
const page1 = await browser.newPage();
//signup user1
await signup(page1, user1.email, user1.password, user1.first_name, user1.last_name)
// Skip signup - use existing user1 account
// await signup(page1, user1.email, user1.password, user1.first_name, user1.last_name)
// ... log in user1
await login(page1, user1.email, user1.password)
await login(page1, user1.email, user1.password)
await page1.context().storageState({ path: 'test/storageState/user1.json' });
const page2 = await browser.newPage();
//signup user2
await signup(page2, user2.email, user2.password, user2.first_name, user2.last_name)
// Skip signup - use existing user2 account
// await signup(page2, user2.email, user2.password, user2.first_name, user2.last_name)
// ... log in
await login(page2, user2.email, user2.password)
await page2.context().storageState({ path: 'test/storageState/user2.json' });
await login(page2, user2.email, user2.password)
await page2.context().storageState({ path: 'test/storageState/user2.json' });
await browser.close();
}

View File

@ -13,27 +13,84 @@ import { APIInterceptor } from '../utils/api-interceptor';
* - Closing metronome
*/
/**
* Helper function to mock jamClient methods
*/
async function mockJamClient(page: Page) {
await page.evaluate(() => {
// Create mock that logs and returns resolved promises
const mockMethods = {
SessionOpenMetronome: async (...args: any[]) => {
console.log('[MOCK] SessionOpenMetronome called', args);
return true;
},
SessionSetMetronome: async (...args: any[]) => {
console.log('[MOCK] SessionSetMetronome called', args);
return true;
},
SessionStopPlay: async (...args: any[]) => {
console.log('[MOCK] SessionStopPlay called', args);
return true;
},
SessionCloseMetronome: async (...args: any[]) => {
console.log('[MOCK] SessionCloseMetronome called', args);
return true;
},
SetVURefreshRate: async (...args: any[]) => {
console.log('[MOCK] SetVURefreshRate called', args);
return true;
},
SessionRegisterCallback: async (...args: any[]) => {
console.log('[MOCK] SessionRegisterCallback called', args);
return true;
}
};
// Replace jamClient methods if it exists
if ((window as any).jamClient) {
Object.assign((window as any).jamClient, mockMethods);
console.log('[MOCK] jamClient methods replaced');
} else {
// Create mock jamClient if it doesn't exist
(window as any).jamClient = mockMethods;
console.log('[MOCK] jamClient created');
}
});
}
/**
* Helper function to open metronome in session
*/
async function openMetronome(page: Page) {
console.log('[TEST] Mocking jamClient...');
// Mock jamClient before opening metronome
await mockJamClient(page);
console.log('[TEST] Clicking Open button...');
// Click the "Open" button
await page.locator('button:has-text("Open")').click();
console.log('[TEST] Waiting for dropdown menu...');
// Wait for dropdown menu
await page.waitForSelector('.dropdown-menu.show');
console.log('[TEST] Clicking Metronome in dropdown...');
// Click "Metronome..." in dropdown
await page.locator('button.dropdown-item:has-text("Metronome")').click();
console.log('[TEST] Waiting for metronome to open...');
// Wait for metronome to open (either popup window or modal)
await page.waitForTimeout(1000);
console.log('[TEST] Metronome open complete');
}
test.describe('Metronome Controls', () => {
let apiInterceptor: APIInterceptor;
test.beforeEach(async ({ page }) => {
// Increase timeout for these tests
test.setTimeout(60000);
// Set up API interceptor
apiInterceptor = new APIInterceptor();
await apiInterceptor.intercept(page);
@ -41,6 +98,9 @@ test.describe('Metronome Controls', () => {
// Login and join a session
await loginToJamUI(page);
await createAndJoinSession(page);
// Mock jamClient after session is created
await mockJamClient(page);
});
test('should open metronome controls', async ({ page }) => {
@ -54,11 +114,11 @@ test.describe('Metronome Controls', () => {
const metronomeTrack = page.locator('.metronomeTrack');
await expect(metronomeTrack).toBeVisible({ timeout: 15000 });
// Verify metronome heading is present
await expect(metronomeTrack.locator('h5:has-text("Metronome")')).toBeVisible();
// Verify metronome title is present
await expect(metronomeTrack.locator('.track-title:has-text("Metronome")')).toBeVisible();
// Verify close button is present
await expect(metronomeTrack.locator('a:has-text("Close")')).toBeVisible();
// Verify three dots menu button is present
await expect(metronomeTrack.locator('.track-menu-button')).toBeVisible();
});
test('should allow adjusting BPM with slider in popup', async ({ page, context }) => {
@ -101,8 +161,14 @@ test.describe('Metronome Controls', () => {
const metronomeTrack = page.locator('.metronomeTrack');
await expect(metronomeTrack).toBeVisible({ timeout: 15000 });
// Click close button
await metronomeTrack.locator('a:has-text("Close")').click();
// Click three dots menu button to open menu
await metronomeTrack.locator('.track-menu-button').click();
// Wait for menu to appear
await page.waitForTimeout(500);
// Click close in the menu
await metronomeTrack.locator('.track-menu div:has-text("Close")').click();
// Wait for close operation
await page.waitForTimeout(2000);