Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions client/src/components/ShareLinkFallback.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { ThemeProvider } from '@mui/material/styles';
import { describe, expect, it, vi } from 'vitest';
import theme from '../theme/theme';
import ShareLinkFallback from './ShareLinkFallback';

const link = 'https://ameet.example/lobby/abc-defg-hij';

function renderFallback(value: string | null, onClose = vi.fn()) {
render(
<ThemeProvider theme={theme}>
<ShareLinkFallback link={value} onClose={onClose} />
</ThemeProvider>,
);
return onClose;
}

describe('ShareLinkFallback', () => {
it('shows the full joining link when automatic copy did not work', () => {
renderFallback(link);
expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: 'Meeting link' })).toHaveValue(link);
expect(screen.getByText(/blocked automatic copy/i)).toBeInTheDocument();
});

it('stays closed when there is no link to show', () => {
renderFallback(null);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

it('closes when the user dismisses it', () => {
const onClose = renderFallback(link);
fireEvent.click(screen.getByRole('button', { name: 'Close' }));
expect(onClose).toHaveBeenCalledOnce();
});
});
39 changes: 39 additions & 0 deletions client/src/components/ShareLinkFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { FocusEvent } from 'react';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from '@mui/material';

interface ShareLinkFallbackProps {
link: string | null;
onClose: () => void;
}

export default function ShareLinkFallback({ link, onClose }: ShareLinkFallbackProps) {
return (
<Dialog
open={Boolean(link)}
onClose={onClose}
slotProps={{ paper: { sx: { borderRadius: 3, minWidth: 320, maxWidth: 480 } } }}
>
<DialogTitle sx={{ pb: 1 }}>Copy this meeting link</DialogTitle>
<DialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1.5 }}>
The browser blocked automatic copy. Select the joining link below.
</Typography>
<TextField
fullWidth
autoFocus
value={link ?? ''}
slotProps={{
htmlInput: {
readOnly: true,
'aria-label': 'Meeting link',
onFocus: (event: FocusEvent<HTMLInputElement>) => event.currentTarget.select(),
},
}}
/>
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<Button onClick={onClose}>Close</Button>
</DialogActions>
</Dialog>
);
}
130 changes: 130 additions & 0 deletions client/src/pages/LandingPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router';
import { ThemeProvider } from '@mui/material/styles';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import theme from '../theme/theme';

const { navigateMock, postMock } = vi.hoisted(() => ({
navigateMock: vi.fn(),
postMock: vi.fn(),
}));

vi.mock('react-router', async (importOriginal) => ({
...(await importOriginal()),
useNavigate: () => navigateMock,
}));

vi.mock('../context/AuthContext', () => ({
useAuth: () => ({
user: { name: 'Ada Lovelace', avatar: '' },
login: vi.fn(),
logout: vi.fn(),
}),
}));

vi.mock('../api/axios', () => ({
default: { post: postMock },
}));

vi.mock('../components/EtherealShadow', () => ({ default: () => null }));
vi.mock('../components/UpcomingMeetings', () => ({ default: () => null }));
vi.mock('../components/ScheduleMeetingDialog', () => ({ default: () => null }));

import LandingPage from './LandingPage';

function renderLanding() {
return render(
<ThemeProvider theme={theme}>
<MemoryRouter>
<LandingPage />
</MemoryRouter>
</ThemeProvider>,
);
}

beforeAll(() => {
window.matchMedia ||= vi.fn().mockImplementation((query) => ({
matches: false, media: query, onchange: null,
addListener: vi.fn(), removeListener: vi.fn(),
addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(),
}));
});

beforeEach(() => {
navigateMock.mockClear();
postMock.mockReset();
});

describe('LandingPage join and create', () => {
it('joins from a pasted invite link instead of keeping the query string', () => {
renderLanding();
fireEvent.change(screen.getByRole('textbox', { name: 'Meeting code or link' }), {
target: { value: 'https://ameet.raja-dev.me/lobby/abc-defg-hij?guest=1' },
});
fireEvent.click(screen.getByRole('button', { name: 'Join' }));
expect(navigateMock).toHaveBeenCalledWith('/lobby/abc-defg-hij');
});

it('joins when the code is typed as three words', () => {
renderLanding();
fireEvent.change(screen.getByRole('textbox', { name: 'Meeting code or link' }), {
target: { value: 'abc defg hij' },
});
fireEvent.click(screen.getByRole('button', { name: 'Join' }));
expect(navigateMock).toHaveBeenCalledWith('/lobby/abc-defg-hij');
});

it('explains a bad code and stays on the page', () => {
renderLanding();
fireEvent.change(screen.getByRole('textbox', { name: 'Meeting code or link' }), {
target: { value: 'not a meeting' },
});
fireEvent.click(screen.getByRole('button', { name: 'Join' }));
expect(navigateMock).not.toHaveBeenCalled();
expect(screen.getByRole('alert')).toHaveTextContent(/meeting code/i);
});

it('tells the user when a new meeting cannot be created', async () => {
postMock.mockRejectedValue(new Error('offline'));
renderLanding();
fireEvent.click(screen.getByRole('button', { name: 'New meeting' }));
expect(await screen.findByRole('alert')).toHaveTextContent(/could not start/i);
expect(navigateMock).not.toHaveBeenCalled();
});

it('keeps a failed create beside New meeting when the join field changes', async () => {
postMock.mockRejectedValue(new Error('offline'));
renderLanding();
fireEvent.click(screen.getByRole('button', { name: 'New meeting' }));
expect(await screen.findByRole('alert')).toHaveTextContent(/could not start/i);

const code = screen.getByRole('textbox', { name: 'Meeting code or link' });
const newMeeting = screen.getByRole('button', { name: 'New meeting' });
fireEvent.change(code, { target: { value: 'not a meeting' } });
fireEvent.click(screen.getByRole('button', { name: 'Join' }));

expect(screen.getByText(/could not start/i)).toBeInTheDocument();
expect(screen.getByText(/meeting code/i)).toBeInTheDocument();
expect(code).toHaveAttribute('aria-describedby', 'join-code-error');
expect(newMeeting).toHaveAttribute('aria-describedby', 'create-meeting-error');
expect(code).not.toHaveAccessibleDescription(/could not start/i);

fireEvent.change(code, { target: { value: 'not a meetin' } });

expect(screen.queryByText(/meeting code/i)).not.toBeInTheDocument();
expect(screen.getByText(/could not start/i)).toBeInTheDocument();
expect(code).not.toHaveAttribute('aria-describedby');
expect(newMeeting).toHaveAttribute('aria-describedby', 'create-meeting-error');
expect(code).not.toHaveAccessibleDescription(/could not start/i);
});

it('opens the room after a meeting is created', async () => {
postMock.mockResolvedValue({ data: { roomId: 'abc-defg-hij' } });
renderLanding();
fireEvent.click(screen.getByRole('button', { name: 'New meeting' }));
await waitFor(() => expect(navigateMock).toHaveBeenCalledWith(
'/room/abc-defg-hij',
{ state: { fromCreate: true } },
));
});
});
Loading
Loading