This commit is contained in:
seavor 2026-04-18 01:36:37 -05:00
parent d04aa83258
commit dcd6dc00f4
83 changed files with 1797 additions and 390 deletions

View file

@ -0,0 +1,33 @@
import { screen } from '@testing-library/react';
import { renderWithProviders, connectedState, disconnectedState } from '../../__test-utils__';
import AuthGuard from './AuthGuard';
vi.mock('@app/hooks', async (importOriginal) => {
const actual = await importOriginal<typeof import('@app/hooks')>();
return { ...actual, useWebClient: vi.fn(() => ({})) };
});
describe('AuthGuard', () => {
it('redirects to LOGIN when disconnected', () => {
renderWithProviders(<AuthGuard />, {
preloadedState: disconnectedState,
route: '/server',
});
// Navigate triggers a route change — AuthGuard itself renders no text.
// We verify it doesn't render any meaningful content.
expect(screen.queryByRole('button')).toBeNull();
expect(screen.queryByRole('heading')).toBeNull();
});
it('renders nothing visible when connected', () => {
const { container } = renderWithProviders(<AuthGuard />, {
preloadedState: connectedState,
route: '/server',
});
// AuthGuard renders an empty fragment when connected.
// The only DOM is from provider wrappers (e.g. ToastProvider's container div).
expect(container.textContent).toBe('');
});
});

View file

@ -8,7 +8,7 @@ const AuthGuard = () => {
const isConnected = useAppSelector(ServerSelectors.getIsConnected);
return !isConnected
? <Navigate to={App.RouteEnum.LOGIN} />
: <div></div>;
: <></>;
};
export default AuthGuard;

View file

@ -0,0 +1,38 @@
import { renderWithProviders, connectedState, makeUser } from '../../__test-utils__';
import { Data } from '@app/types';
import ModGuard from './ModGuard';
vi.mock('@app/hooks', async (importOriginal) => {
const actual = await importOriginal<typeof import('@app/hooks')>();
return { ...actual, useWebClient: vi.fn(() => ({})) };
});
describe('ModGuard', () => {
it('redirects when user is not a moderator', () => {
const { container } = renderWithProviders(<ModGuard />, {
preloadedState: connectedState,
route: '/logs',
});
expect(container.textContent).toBe('');
});
it('renders nothing visible when user is a moderator', () => {
const modUser = makeUser({
userLevel: Data.ServerInfo_User_UserLevelFlag.IsModerator,
});
const { container } = renderWithProviders(<ModGuard />, {
preloadedState: {
...connectedState,
server: {
...(connectedState.server as any),
user: modUser,
},
},
route: '/logs',
});
expect(container.textContent).toBe('');
});
});

View file

@ -0,0 +1,30 @@
import { render, screen } from '@testing-library/react';
import InputField from './InputField';
describe('InputField', () => {
const defaultProps = {
input: { name: 'test', value: '', onChange: vi.fn(), onBlur: vi.fn(), onFocus: vi.fn() },
meta: { touched: false, error: null, warning: null },
label: 'Test Field',
};
it('renders a text field with label', () => {
render(<InputField {...defaultProps} />);
expect(screen.getByRole('textbox')).toBeInTheDocument();
});
it('shows error when touched and has error', () => {
render(<InputField {...defaultProps} meta={{ touched: true, error: 'Required', warning: null }} />);
expect(screen.getByText('Required')).toBeInTheDocument();
});
it('shows warning when touched and has warning', () => {
render(<InputField {...defaultProps} meta={{ touched: true, error: null, warning: 'Weak password' }} />);
expect(screen.getByText('Weak password')).toBeInTheDocument();
});
it('does not show validation messages when not touched', () => {
render(<InputField {...defaultProps} meta={{ touched: false, error: 'Required', warning: null }} />);
expect(screen.queryByText('Required')).not.toBeInTheDocument();
});
});

View file

@ -16,7 +16,7 @@ import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined';
import { useWebClient } from '@app/hooks';
import { KnownHostDialog } from '@app/dialogs';
import { useReduxEffect } from '@app/hooks';
import { HostDTO } from '@app/services';
import { DefaultHosts, HostDTO, getHostPort } from '@app/services';
import { ServerTypes } from '@app/store';
import { App } from '@app/types';
import Toast from '../Toast/Toast';
@ -87,7 +87,7 @@ const KnownHosts = (props) => {
if (!hosts?.length) {
// @TODO: find a better pattern to seeding default data in indexedDB
await HostDTO.bulkAdd(App.DefaultHosts);
await HostDTO.bulkAdd(DefaultHosts);
loadKnownHosts();
} else {
const selectedHost = hosts.find(({ lastSelected }) => lastSelected) || hosts[0];
@ -197,7 +197,7 @@ const KnownHosts = (props) => {
const testConnection = () => {
setTestingConnection(TestConnection.TESTING);
const options = { ...App.getHostPort(hostsState.selectedHost) };
const options = { ...getHostPort(hostsState.selectedHost) };
webClient.request.authentication.testConnection(options);
}
@ -238,7 +238,7 @@ const KnownHosts = (props) => {
{
hostsState.hosts.map((host, index) => {
const hostPort = App.getHostPort(hostsState.hosts[index]);
const hostPort = getHostPort(hostsState.hosts[index]);
return (
<MenuItem value={host} key={index}>

View file

@ -0,0 +1,19 @@
import { screen } from '@testing-library/react';
import { renderWithProviders } from '../../__test-utils__';
import Message from './Message';
describe('Message', () => {
it('renders a plain message', () => {
const message = { message: 'Hello world' };
renderWithProviders(<Message message={message} />);
expect(screen.getByText('Hello world')).toBeInTheDocument();
});
it('renders the message container', () => {
const message = { message: 'Test message' };
const { container } = renderWithProviders(<Message message={message} />);
expect(container.querySelector('.message')).toBeInTheDocument();
});
});

View file

@ -3,8 +3,7 @@ import Grid from '@mui/material/Grid';
import './ThreePaneLayout.css';
// @DEPRECATED
// This component sucks balls, dont use it. It will be removed sooner than later.
/** @deprecated Scheduled for replacement with a more flexible layout component. */
function ThreePaneLayout(props: ThreePaneLayoutProps) {
return (
<div className="three-pane-layout">

View file

@ -0,0 +1,46 @@
import { screen } from '@testing-library/react';
import { renderWithProviders, connectedState, makeUser, createMockWebClient } from '../../__test-utils__';
import UserDisplay from './UserDisplay';
const mockWebClient = createMockWebClient();
vi.mock('@app/hooks', async (importOriginal) => {
const actual = await importOriginal<typeof import('@app/hooks')>();
return { ...actual, useWebClient: vi.fn(() => mockWebClient) };
});
vi.mock('@app/images', () => ({
Images: { Countries: { us: 'us.png', de: 'de.png' } },
}));
describe('UserDisplay', () => {
it('renders user name', () => {
const user = makeUser({ name: 'TestPlayer', country: 'us' });
renderWithProviders(<UserDisplay user={user} />, {
preloadedState: connectedState,
});
expect(screen.getByText('TestPlayer')).toBeInTheDocument();
});
it('renders country flag image', () => {
const user = makeUser({ name: 'TestPlayer', country: 'us' });
renderWithProviders(<UserDisplay user={user} />, {
preloadedState: connectedState,
});
const img = screen.getByAltText('us');
expect(img).toBeInTheDocument();
expect(img).toHaveAttribute('src', 'us.png');
});
it('renders link to player profile', () => {
const user = makeUser({ name: 'TestPlayer', country: 'us' });
renderWithProviders(<UserDisplay user={user} />, {
preloadedState: connectedState,
});
const link = screen.getByRole('link', { name: /TestPlayer/ });
expect(link).toHaveAttribute('href', '/player/TestPlayer');
});
});

View file

@ -0,0 +1,21 @@
import { render } from '@testing-library/react';
import VirtualList from './VirtualList';
describe('VirtualList', () => {
it('renders without crashing with empty items', () => {
const { container } = render(<VirtualList items={[]} />);
expect(container.querySelector('.virtual-list')).toBeInTheDocument();
});
it('accepts className as a string', () => {
const { container } = render(<VirtualList items={[]} className="custom-class" />);
expect(container.querySelector('.custom-class')).toBeInTheDocument();
});
it('applies empty string as default className (not object)', () => {
const { container } = render(<VirtualList items={[]} />);
const list = container.querySelector('.virtual-list__list');
// className should not contain "[object Object]"
expect(list?.className).not.toContain('[object Object]');
});
});

View file

@ -15,7 +15,7 @@ const Row = ({ index, style, items }: RowComponentProps<RowData>) => (
</div>
);
const VirtualList = ({ items, className = {}, size = 30 }) => (
const VirtualList = ({ items, className = '', size = 30 }) => (
<div className="virtual-list">
<List<RowData>
className={`virtual-list__list ${className}`}