Copy Link button implemented
This commit is contained in:
58
frontend/src/components/CopyLinkButton.tsx
Normal file
58
frontend/src/components/CopyLinkButton.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, SxProps, Theme } from '@mui/material';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
||||
|
||||
interface CopyLinkButtonProps {
|
||||
linkToCopy: string;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
const CopyLinkButton: React.FC<CopyLinkButtonProps> = ({ linkToCopy, sx }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
||||
alert('Clipboard API not supported in this browser. Please copy the link manually:' + linkToCopy);
|
||||
console.warn('Clipboard API not supported.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(linkToCopy);
|
||||
setCopied(true);
|
||||
console.log('Link copied to clipboard:', linkToCopy);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy link:', err);
|
||||
alert('Failed to copy the link. Please try again or copy manually.');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (copied) {
|
||||
timer = setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 3000); // Revert after 3 seconds
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleCopy}
|
||||
startIcon={copied ? <CheckCircleOutlineIcon color="success" /> : <ContentCopyIcon />}
|
||||
color={copied ? "success" : "primary"}
|
||||
sx={{
|
||||
minWidth: 120,
|
||||
textTransform: 'none',
|
||||
...(sx ? sx : {}),
|
||||
}}
|
||||
>
|
||||
{copied ? 'Copied!' : 'Copy Link'}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default CopyLinkButton;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography, List, ListItem, ListItemText, Collapse, IconButton } from '@mui/material';
|
||||
import { Box, Typography, Collapse, IconButton } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { Decision } from '../hooks/useSession';
|
||||
@@ -43,7 +43,7 @@ const ResultsDisplay: React.FC<ResultsDisplayProps> = ({ decision }) => {
|
||||
return (
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
Cooperative Decision
|
||||
Harmonized Desires
|
||||
</Typography>
|
||||
|
||||
<CategorySection title="Go-to" desire={decision.goTo} borderColor="#a5d6a7" /> {/* Pale Green */}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Container, Typography, Box, CircularProgress, Alert, TextField, Button } from '@mui/material';
|
||||
import { useSession, Session, Participant, DesireSet, Decision, SessionState } from '../hooks/useSession';
|
||||
import { webSocketService } from '../services/websocket';
|
||||
import { useSession, DesireSet, SessionState } from '../hooks/useSession';
|
||||
import DesireForm from '../components/DesireForm';
|
||||
import ResultsDisplay from '../components/ResultsDisplay';
|
||||
import CopyLinkButton from '../components/CopyLinkButton'; // Import CopyLinkButton
|
||||
|
||||
const SessionPage = () => {
|
||||
const { sessionId } = useParams<{ sessionId: string }>();
|
||||
const [session, setSession, sendMessage, clientId, wsError] = useSession(sessionId || '');
|
||||
const [session, , sendMessage, clientId, ] = useSession(sessionId || '');
|
||||
const [expectedResponses, setExpectedResponses] = useState(2);
|
||||
const [expectedResponsesError, setExpectedResponsesError] = useState(false);
|
||||
const [topic, setTopic] = useState('');
|
||||
@@ -56,7 +56,7 @@ const SessionPage = () => {
|
||||
<Box sx={{ mt: 4 }}>
|
||||
|
||||
{session.state === SessionState.SETUP && (
|
||||
<Box sx={{ mt: 4, p: 3, textAlign: 'center' }}>
|
||||
<Box sx={{ mt: 4, p: 3, textAlign: 'left' }}>
|
||||
<Typography variant="h5" component="h5" gutterBottom>
|
||||
Harmonize Desires
|
||||
</Typography>
|
||||
@@ -122,9 +122,12 @@ const SessionPage = () => {
|
||||
|
||||
{session.state !== SessionState.SETUP && (
|
||||
<Box sx={{ mt: 2, p: 2, border: '1px solid #e0e0e0', borderRadius: '4px', backgroundColor: '#f5f5f5' }}>
|
||||
<Typography variant="h4" component="h4" gutterBottom>
|
||||
{session.topic || session.sessionId}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Typography variant="h4" component="h4" gutterBottom>
|
||||
{session.topic || session.sessionId}
|
||||
</Typography>
|
||||
<CopyLinkButton linkToCopy={window.location.href} />
|
||||
</Box>
|
||||
{session.description && (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Details: {session.description}
|
||||
|
||||
78
frontend/tests/CopyLinkButton.test.tsx
Normal file
78
frontend/tests/CopyLinkButton.test.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import CopyLinkButton from '../src/components/CopyLinkButton';
|
||||
|
||||
// Mock the Clipboard API
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('CopyLinkButton', () => {
|
||||
const testLink = 'http://localhost:3000/session/123';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('renders with "Copy Link" text and copy icon initially', () => {
|
||||
render(<CopyLinkButton linkToCopy={testLink} />);
|
||||
expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ContentCopyIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('copies the link to clipboard and shows "Copied!" with check icon on click', async () => {
|
||||
render(<CopyLinkButton linkToCopy={testLink} />);
|
||||
const copyButton = screen.getByRole('button', { name: /copy link/i });
|
||||
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(testLink);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('CheckCircleOutlineIcon')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('reverts to "Copy Link" and copy icon after 3 seconds', async () => {
|
||||
jest.useFakeTimers();
|
||||
render(<CopyLinkButton linkToCopy={testLink} />);
|
||||
const copyButton = screen.getByRole('button', { name: /copy link/i });
|
||||
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ContentCopyIcon')).toBeInTheDocument();
|
||||
});
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('logs an error if clipboard.writeText fails', async () => {
|
||||
const errorMessage = 'Failed to write to clipboard';
|
||||
(navigator.clipboard.writeText as jest.Mock).mockRejectedValueOnce(new Error(errorMessage));
|
||||
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
render(<CopyLinkButton linkToCopy={testLink} />);
|
||||
const copyButton = screen.getByRole('button', { name: /copy link/i });
|
||||
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Failed to copy link:', expect.any(Error));
|
||||
});
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,51 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import ResultsDisplay from '../src/components/ResultsDisplay';
|
||||
import { Decision } from '../src/hooks/useSession';
|
||||
|
||||
// Mock the Clipboard API
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('ResultsDisplay responsiveness and touch functionality', () => {
|
||||
const mockDecision: Decision = {
|
||||
goTo: 'Go-to decision',
|
||||
alsoGood: 'Also good decision',
|
||||
considerable: 'Considerable decision',
|
||||
needsDiscussion: 'Needs discussion decision',
|
||||
noGoes: 'No-goes decision',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(<ResultsDisplay />);
|
||||
// Add assertions specific to ResultsDisplay
|
||||
render(<ResultsDisplay decision={mockDecision} />);
|
||||
expect(screen.getByText('Cooperative Decision')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the Copy Link button', () => {
|
||||
render(<ResultsDisplay decision={mockDecision} />);
|
||||
expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ContentCopyIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should copy the session URL to clipboard when Copy Link button is clicked', async () => {
|
||||
render(<ResultsDisplay decision={mockDecision} />);
|
||||
const copyButton = screen.getByRole('button', { name: /copy link/i });
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(window.location.href);
|
||||
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Placeholder for responsive tests
|
||||
|
||||
@@ -1,36 +1,102 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useSession, SessionState } from '../src/hooks/useSession';
|
||||
import SessionPage from '../src/pages/SessionPage';
|
||||
|
||||
// Mock the useParams hook
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn(),
|
||||
}));
|
||||
|
||||
// Mock the useSession hook
|
||||
jest.mock('../src/hooks/useSession', () => ({
|
||||
...jest.requireActual('../src/hooks/useSession'),
|
||||
useSession: jest.fn(),
|
||||
SessionState: jest.requireActual('../src/hooks/useSession').SessionState,
|
||||
}));
|
||||
|
||||
// Mock the Clipboard API
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
writeText: jest.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
|
||||
describe('SessionPage functionality and readability', () => {
|
||||
const mockSession = {
|
||||
sessionId: 'test-session-id',
|
||||
topic: 'Test Session Topic',
|
||||
description: 'Test Description',
|
||||
expectedResponses: 2,
|
||||
submittedCount: 0,
|
||||
state: SessionState.GATHERING,
|
||||
responses: {},
|
||||
finalResult: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
(useParams as jest.Mock).mockReturnValue({ sessionId: 'test-session-id' });
|
||||
(useSession as jest.Mock).mockReturnValue([
|
||||
mockSession,
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
'client123',
|
||||
null,
|
||||
]);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(<SessionPage />);
|
||||
// Add assertions specific to SessionPage
|
||||
expect(screen.getByText('Test Session Topic')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not display "Session:" prefix for the session topic', () => {
|
||||
render(<SessionPage />);
|
||||
// Assuming a session topic is present, verify "Session:" is not in the document
|
||||
expect(screen.queryByText(/Session: /i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not use placeholders in input fields and display validation rules', () => {
|
||||
// Mock session to be in SETUP state for this test
|
||||
(useSession as jest.Mock).mockReturnValue([
|
||||
{ ...mockSession, state: SessionState.SETUP },
|
||||
jest.fn(),
|
||||
jest.fn(),
|
||||
'client123',
|
||||
null,
|
||||
]);
|
||||
render(<SessionPage />);
|
||||
// Check for TextField in SETUP state
|
||||
// Assuming the component is in SETUP state for this test
|
||||
// For 'Session Topic' TextField
|
||||
expect(screen.queryByPlaceholderText(/Session Topic/i)).not.toBeInTheDocument();
|
||||
// For 'Number of Expected Responses' TextField
|
||||
expect(screen.queryByPlaceholderText(/Number of Expected Responses/i)).not.toBeInTheDocument();
|
||||
// Add checks for helperText if validation rules are added to these fields
|
||||
});
|
||||
|
||||
it('should hide the session status display', () => {
|
||||
render(<SessionPage />);
|
||||
// Verify that the element displaying "Status:" is not in the document
|
||||
expect(screen.queryByText(/Status:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display the Copy Link button when not in SETUP state', () => {
|
||||
render(<SessionPage />);
|
||||
expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ContentCopyIcon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should copy the session URL to clipboard when Copy Link button is clicked', async () => {
|
||||
render(<SessionPage />);
|
||||
const copyButton = screen.getByRole('button', { name: /copy link/i });
|
||||
fireEvent.click(copyButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(window.location.href);
|
||||
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// Placeholder for responsive tests
|
||||
it('should adapt layout for mobile view', () => {
|
||||
// Simulate mobile viewport and check for specific layout changes
|
||||
|
||||
Reference in New Issue
Block a user