diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index ca35823..3a81963 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,6 @@ // src/App.jsx -import React, { useEffect, useState } from 'react'; -import { Routes, Route } from 'react-router-dom'; +import React, { useState } from 'react'; +import { Routes, Route, useLocation } from 'react-router-dom'; import { CssBaseline, ThemeProvider, @@ -12,17 +12,14 @@ import Lobby from './components/Lobby'; import Navbar from './components/Navbar'; import LandingPage from './components/LandingPage'; import About from './components/About'; -import useWebex from './hooks/useWebex'; import { ROUTES } from './constants'; function App() { - const { theme: webexTheme } = useWebex(); const [darkMode, setDarkMode] = useState(true); // Set dark mode as default + const location = useLocation(); - useEffect(() => { - // Only switch from dark if webex theme explicitly requests light - setDarkMode(webexTheme !== 'light'); - }, [webexTheme]); + // Only load Webex on /game routes + const shouldLoadWebex = location.pathname.startsWith('/game'); const theme = createTheme({ palette: { @@ -43,7 +40,11 @@ function App() { return ( - + } /> diff --git a/frontend/src/components/CreateLobby.jsx b/frontend/src/components/CreateLobby.jsx index 38211df..dd68dfd 100644 --- a/frontend/src/components/CreateLobby.jsx +++ b/frontend/src/components/CreateLobby.jsx @@ -8,6 +8,8 @@ import { CircularProgress, Box, Paper, + FormControlLabel, + Checkbox, } from '@mui/material'; import { v4 as uuidv4 } from 'uuid'; import { ROUTES } from '../constants'; @@ -20,12 +22,19 @@ const CreateLobby = () => { const [lobbyName, setLobbyName] = useState(''); const [displayName, setDisplayName] = useState(''); const [loading, setLoading] = useState(false); + + // Initialize webexEnabled based on URL parameter + const [webexEnabled, setWebexEnabled] = useState(() => { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get('disableWebex') !== 'true'; + }); + const { isLoading, username, meetingName } = useWebex(); // Once isLoading is false, set default values from Webex SDK // This is a workaround to avoid setting default values before Webex SDK is ready useEffect(() => { - if (!isLoading) { + if (!isLoading && webexEnabled) { if (meetingName) { setLobbyName(meetingName); } @@ -33,7 +42,7 @@ const CreateLobby = () => { setDisplayName(username); } } - }, [isLoading, meetingName, username]); + }, [isLoading, meetingName, username, webexEnabled]); const handleCreateLobby = async () => { if (!lobbyName.trim() || !displayName.trim()) return; @@ -42,7 +51,14 @@ const CreateLobby = () => { try { const hostId = uuidv4(); const data = await api.createLobby(hostId, displayName, lobbyName); - navigate(ROUTES.GAME_WITH_ID(data.lobby_id), { + + // Build the game URL with optional disableWebex parameter + let gameUrl = ROUTES.GAME_WITH_ID(data.lobby_id); + if (!webexEnabled) { + gameUrl += '?disableWebex=true'; + } + + navigate(gameUrl, { state: { user: { id: hostId, display_name: displayName } }, }); } catch (error) { @@ -99,6 +115,27 @@ const CreateLobby = () => { value={displayName} onChange={(e) => setDisplayName(e.target.value)} /> + + setWebexEnabled(e.target.checked)} + sx={{ + color: 'primary.main', + '&.Mui-checked': { + color: 'primary.main', + }, + }} + /> + } + label={ + + Enable Webex Integration + + } + /> + + + + diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 03f4496..9c172be 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -26,9 +26,15 @@ import { Link as RouterLink } from 'react-router-dom'; import { ROUTES } from '../constants'; import AboutModal from './About/AboutModal'; -export default function Navbar({ darkMode, setDarkMode }) { +export default function Navbar({ + darkMode, + setDarkMode, + shouldLoadWebex = false, +}) { const [anchorEl, setAnchorEl] = useState(null); const [aboutModalOpen, setAboutModalOpen] = useState(false); + + // Always call the hook, but it will internally skip initialization if disabled const { isConnected, isRunningInWebex, @@ -126,64 +132,70 @@ export default function Navbar({ darkMode, setDarkMode }) { - {/* Webex Info Menu */} - - - - - - - - {loading ? ( - - - - ) : ( - [ - - {isConnected ? ( - - ) : ( - - )} - {isConnected ? 'Connected to Webex' : 'Webex Not Connected'} - , - - !isRunningInWebex && ( - - - Running Outside Webex - - ), - - username && {username}, - - meetingName && ( - {meetingName} - ), - - error && ( - - - {error} + {/* Webex Info Menu - Only show on /game routes */} + {shouldLoadWebex && ( + <> + + + + + + + + {loading ? ( + + - ), - ] - )} - + ) : ( + [ + + {isConnected ? ( + + ) : ( + + )} + {isConnected + ? 'Connected to Webex' + : 'Webex Not Connected'} + , + + !isRunningInWebex && ( + + + Running Outside Webex + + ), + + username && {username}, + + meetingName && ( + {meetingName} + ), + + error && ( + + + {error} + + ), + ] + )} + + + )} @@ -197,4 +209,5 @@ export default function Navbar({ darkMode, setDarkMode }) { Navbar.propTypes = { darkMode: PropTypes.bool.isRequired, setDarkMode: PropTypes.func.isRequired, + shouldLoadWebex: PropTypes.bool, }; diff --git a/frontend/src/components/__tests__/LandingPage.test.jsx b/frontend/src/components/__tests__/LandingPage.test.jsx index 67f8903..f16e208 100644 --- a/frontend/src/components/__tests__/LandingPage.test.jsx +++ b/frontend/src/components/__tests__/LandingPage.test.jsx @@ -23,15 +23,31 @@ describe('LandingPage', () => { expect(screen.getByText(/redshift/i)).toBeInTheDocument(); }); - it('renders Create a Game button', () => { - const button = screen.getByRole('button', { name: /create a game/i }); - expect(button).toBeInTheDocument(); + it('renders both launch buttons', () => { + const webexButton = screen.getByRole('button', { + name: /launch in webex/i, + }); + const standaloneButton = screen.getByRole('button', { + name: /standalone browser/i, + }); + + expect(webexButton).toBeInTheDocument(); + expect(standaloneButton).toBeInTheDocument(); }); - it('calls navigate("/game") on button click', () => { - const button = screen.getByRole('button', { name: /create a game/i }); + it('calls navigate("/game") when Launch in Webex is clicked', () => { + const button = screen.getByRole('button', { name: /launch in webex/i }); fireEvent.click(button); expect(globalThis.mockNavigate).toHaveBeenCalledWith('/game'); }); + + it('calls navigate("/game?disableWebex=true") when Standalone Browser is clicked', () => { + const button = screen.getByRole('button', { name: /standalone browser/i }); + fireEvent.click(button); + + expect(globalThis.mockNavigate).toHaveBeenCalledWith( + '/game?disableWebex=true', + ); + }); }); diff --git a/frontend/src/context/LobbyProvider.jsx b/frontend/src/context/LobbyProvider.jsx index 692afe4..c899fe1 100644 --- a/frontend/src/context/LobbyProvider.jsx +++ b/frontend/src/context/LobbyProvider.jsx @@ -30,10 +30,15 @@ export const LobbyProvider = ({ lobbyId, initialUser, children }) => { [], ); - const lobbyUrl = useMemo( - () => `${window.location.origin}${ROUTES.GAME_WITH_ID(lobbyId)}`, - [lobbyId], - ); + const lobbyUrl = useMemo(() => { + const baseUrl = `${window.location.origin}${ROUTES.GAME_WITH_ID(lobbyId)}`; + // Preserve disableWebex query parameter if present + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('disableWebex') === 'true') { + return `${baseUrl}?disableWebex=true`; + } + return baseUrl; + }, [lobbyId]); // Fetch initial lobby data useEffect(() => { diff --git a/frontend/src/hooks/__tests__/useWebex.test.js b/frontend/src/hooks/__tests__/useWebex.test.js index 1756eec..a262466 100644 --- a/frontend/src/hooks/__tests__/useWebex.test.js +++ b/frontend/src/hooks/__tests__/useWebex.test.js @@ -44,6 +44,16 @@ describe('useWebex (real hook, mocked SDK)', () => { }); globalThis.mockAppInstance.clearShareUrl.mockResolvedValue(); globalThis.mockAppInstance.setShareUrl.mockResolvedValue(); + + // Mock window.location to be on /game route by default + Object.defineProperty(window, 'location', { + writable: true, + value: { + pathname: '/game', + search: '', + origin: 'http://localhost', + }, + }); }); it('sets Webex state and returns expected values after init', async () => { @@ -68,16 +78,18 @@ describe('useWebex (real hook, mocked SDK)', () => { await waitFor(() => expect(result.current.loading).toBe(false)); - expect(result.current.error).toMatch(/Webex init failed/); + expect(result.current.error).toBe('Webex init failed'); expect(result.current.isConnected).toBe(false); expect(result.current.isRunningInWebex).toBe(false); }); it('disables Webex SDK initialization when query parameter is set', async () => { // Mock the URL to include the disableWebex query parameter - const originalLocation = window.location; - delete window.location; - window.location = new URL('http://localhost?disableWebex=true'); + window.location = { + pathname: '/game', + search: '?disableWebex=true', + origin: 'http://localhost', + }; const { result } = renderHook(() => useWebex()); @@ -87,8 +99,22 @@ describe('useWebex (real hook, mocked SDK)', () => { expect(result.current.username).toBe('Unknown User (Webex Disabled)'); expect(result.current.meetingName).toBe('No Active Meeting'); expect(result.current.theme).toBe('dark'); // Updated to expect dark theme + }); + + it('skips Webex SDK initialization when not on /game route', async () => { + // Mock the URL to be on the landing page + window.location = { + pathname: '/', + search: '', + origin: 'http://localhost', + }; + + const { result } = renderHook(() => useWebex()); + + await waitFor(() => expect(result.current.loading).toBe(false)); - // Restore the original location object - window.location = originalLocation; + expect(result.current.isRunningInWebex).toBe(false); + expect(result.current.isConnected).toBe(false); + expect(result.current.theme).toBe('dark'); }); }); diff --git a/frontend/src/hooks/useWebex.jsx b/frontend/src/hooks/useWebex.jsx index bf623e6..fd057fb 100644 --- a/frontend/src/hooks/useWebex.jsx +++ b/frontend/src/hooks/useWebex.jsx @@ -27,19 +27,24 @@ const useWebex = () => { const [meetingName, setMeetingName] = useState(null); const [theme, setTheme] = useState('dark'); // Default to dark theme for hacker aesthetic + // Only load Webex SDK on /game routes + const shouldLoadWebex = window.location.pathname.startsWith('/game'); + // Check for query parameter to disable Webex const isWebexDisabled = new URLSearchParams(window.location.search).get('disableWebex') === 'true'; useEffect(() => { const initializeWebex = async () => { - // If Webex is disabled via query parameter, skip initialization - if (isWebexDisabled) { + // Skip initialization if not on /game route or if disabled via query parameter + if (!shouldLoadWebex || isWebexDisabled) { setIsRunningInWebex(false); setLoading(false); - setUsername('Unknown User (Webex Disabled)'); - setMeetingName('No Active Meeting'); - setTheme('dark'); // Default to dark theme even when Webex is disabled + if (isWebexDisabled) { + setUsername('Unknown User (Webex Disabled)'); + setMeetingName('No Active Meeting'); + } + setTheme('dark'); // Default to dark theme return; } @@ -98,7 +103,7 @@ const useWebex = () => { }; initializeWebex(); - }, [isWebexDisabled]); + }, [isWebexDisabled, shouldLoadWebex]); /** * Toggles the shared state of the lobby.