1. Keep session alive with ping-pong. 2. Refreshed tests.
This commit is contained in:
10
frontend/build/asset-manifest.json
Normal file
10
frontend/build/asset-manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"files": {
|
||||
"main.js": "/static/js/main.d2d83152.js",
|
||||
"index.html": "/index.html",
|
||||
"main.d2d83152.js.map": "/static/js/main.d2d83152.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/js/main.d2d83152.js"
|
||||
]
|
||||
}
|
||||
1
frontend/build/index.html
Normal file
1
frontend/build/index.html
Normal file
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/logo.svg"/><meta name="theme-color" content="#000000"/><meta name="description" content="A real-time app for collaborative decision-making."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><script src="/config.js"></script><title>Unisono</title><script defer="defer" src="/static/js/main.d2d83152.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
5
frontend/build/logo-white.svg
Normal file
5
frontend/build/logo-white.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="512" height="512" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 25 L 90 25" stroke="white" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M10 50 C 40 50, 60 35, 90 25" stroke="white" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M10 75 C 40 75, 60 45, 90 25" stroke="white" stroke-width="7" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 387 B |
5
frontend/build/logo.svg
Normal file
5
frontend/build/logo.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="512" height="512" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 25 L 90 25" stroke="#6750A4" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M10 50 C 40 50, 60 35, 90 25" stroke="#6750A4" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M10 75 C 40 75, 60 45, 90 25" stroke="#6750A4" stroke-width="7" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 393 B |
3
frontend/build/static/js/main.d2d83152.js
Normal file
3
frontend/build/static/js/main.d2d83152.js
Normal file
File diff suppressed because one or more lines are too long
80
frontend/build/static/js/main.d2d83152.js.LICENSE.txt
Normal file
80
frontend/build/static/js/main.d2d83152.js.LICENSE.txt
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @license React
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-is.production.js
|
||||
*
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @license React
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @remix-run/router v1.23.0
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* React Router v6.30.1
|
||||
*
|
||||
* Copyright (c) Remix Software Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE.md file in the root directory of this source tree.
|
||||
*
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
1
frontend/build/static/js/main.d2d83152.js.map
Normal file
1
frontend/build/static/js/main.d2d83152.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -30,6 +30,11 @@
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint src/**/*.{js,jsx,ts,tsx}"
|
||||
},
|
||||
"jest": {
|
||||
"transformIgnorePatterns": [
|
||||
"node_modules/(?!(axios)/)"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import DesireForm from './DesireForm';
|
||||
|
||||
@@ -48,13 +48,29 @@ describe('DesireForm', () => {
|
||||
render(<DesireForm onSubmit={handleSubmit} />);
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
|
||||
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Please enter at least one desire in any category.')).toBeInTheDocument();
|
||||
});
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('shows alert if conflicting desires are entered', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
render(<DesireForm onSubmit={handleSubmit} />);
|
||||
const wantsInput = screen.getByRole('textbox', { name: /Enter items you want/i });
|
||||
const acceptsInput = screen.getByRole('textbox', { name: /Enter items you accept/i });
|
||||
const submitButton = screen.getByRole('button', { name: /Submit Desires/i });
|
||||
|
||||
await userEvent.type(wantsInput, 'Pizza');
|
||||
await userEvent.type(acceptsInput, 'Pizza');
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(alertMock).toHaveBeenCalledWith('Please enter at least one desire in any category.');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('You have conflicting desires (same item in different categories). Please resolve.')).toBeInTheDocument();
|
||||
});
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
|
||||
alertMock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit, externalError }) => {
|
||||
value={wants}
|
||||
onChange={(e) => setWants(e.target.value)}
|
||||
margin="normal"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
inputProps={{ maxLength: 500, 'aria-label': 'Enter items you want' }}
|
||||
helperText={`Enter items you want, one per line. Max 500 characters per item. ${wants.length}/500`}
|
||||
/>
|
||||
|
||||
@@ -85,7 +85,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit, externalError }) => {
|
||||
value={afraidToAsk}
|
||||
onChange={(e) => setAfraidToAsk(e.target.value)}
|
||||
margin="normal"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
inputProps={{ maxLength: 500, 'aria-label': 'Enter sensitive ideas privately' }}
|
||||
helperText={`Enter sensitive ideas privately. Max 500 characters. ${afraidToAsk.length}/500`}
|
||||
/>
|
||||
|
||||
@@ -97,7 +97,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit, externalError }) => {
|
||||
value={accepts}
|
||||
onChange={(e) => setAccepts(e.target.value)}
|
||||
margin="normal"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
inputProps={{ maxLength: 500, 'aria-label': 'Enter items you accept' }}
|
||||
helperText={`Enter items you accept, one per line. Max 500 characters per item. ${accepts.length}/500`}
|
||||
/>
|
||||
|
||||
@@ -109,7 +109,7 @@ const DesireForm: React.FC<DesireFormProps> = ({ onSubmit, externalError }) => {
|
||||
value={noGoes}
|
||||
onChange={(e) => setNoGoes(e.target.value)}
|
||||
margin="normal"
|
||||
inputProps={{ maxLength: 500 }}
|
||||
inputProps={{ maxLength: 500, 'aria-label': 'Enter items you absolutely do not want' }}
|
||||
helperText={`Enter items you absolutely do not want, one per line. Max 500 characters per item. ${noGoes.length}/500`}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,22 +1,41 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import CreateSession from './CreateSession';
|
||||
import axios from 'axios';
|
||||
|
||||
test('renders create session page with a form', () => {
|
||||
render(<CreateSession />);
|
||||
|
||||
// Check for a heading
|
||||
const headingElement = screen.getByText(/Create a New Session/i);
|
||||
expect(headingElement).toBeInTheDocument();
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>;
|
||||
|
||||
// Check for form fields
|
||||
const topicInput = screen.getByLabelText(/Topic/i);
|
||||
expect(topicInput).toBeInTheDocument();
|
||||
// Mock useNavigate
|
||||
const mockedUseNavigate = jest.fn();
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockedUseNavigate,
|
||||
}));
|
||||
|
||||
const participantsInput = screen.getByLabelText(/Number of Participants/i);
|
||||
expect(participantsInput).toBeInTheDocument();
|
||||
describe('CreateSession', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.clearAllMocks();
|
||||
mockedAxios.post.mockResolvedValue({ data: { sessionId: 'test-session-id' } });
|
||||
});
|
||||
|
||||
// Check for the create button
|
||||
const createButton = screen.getByRole('button', { name: /Create Session/i });
|
||||
expect(createButton).toBeInTheDocument();
|
||||
test('renders loading state initially and then navigates to session page on successful creation', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<CreateSession />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
// Initially, it should show the loading state
|
||||
expect(screen.getByText(/Creating a new session.../i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('progressbar')).toBeInTheDocument();
|
||||
|
||||
// After session creation, it should navigate
|
||||
await waitFor(() => {
|
||||
expect(mockedUseNavigate).toHaveBeenCalledWith('/session/test-session-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ class WebSocketService {
|
||||
private sessionTerminatedHandlers: (() => void)[] = [];
|
||||
private currentSessionId: string | null = null;
|
||||
private currentClientId: string | null = null;
|
||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
connect(sessionId: string, clientId: string) {
|
||||
// Prevent multiple connections
|
||||
@@ -33,6 +34,13 @@ class WebSocketService {
|
||||
console.log('WebSocket connected');
|
||||
// Directly send registration message on open
|
||||
this.sendMessage({ type: 'REGISTER_CLIENT' });
|
||||
|
||||
// Start heartbeat to keep connection alive
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'PING', clientId: this.currentClientId, sessionId: this.currentSessionId }));
|
||||
}
|
||||
}, 30000); // Send ping every 30 seconds
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
@@ -47,6 +55,10 @@ class WebSocketService {
|
||||
|
||||
this.ws.onclose = () => {
|
||||
console.log('WebSocket disconnected');
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
this.sessionTerminatedHandlers.forEach(handler => handler());
|
||||
this.ws = null;
|
||||
this.currentSessionId = null;
|
||||
@@ -55,14 +67,26 @@ class WebSocketService {
|
||||
|
||||
this.ws.onerror = (event) => {
|
||||
console.error('WebSocket error:', event);
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
this.errorHandlers.forEach(handler => handler(event));
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
}
|
||||
if (this.heartbeatInterval) {
|
||||
clearInterval(this.heartbeatInterval);
|
||||
this.heartbeatInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(message: any) {
|
||||
|
||||
1
frontend/src/setupTests.ts
Normal file
1
frontend/src/setupTests.ts
Normal file
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom';
|
||||
Reference in New Issue
Block a user