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
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bunx lint-staged
npx lint-staged
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,18 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^16.4.0",
"prettier": "^3.8.3",
"typescript-eslint": "^8.58.2",
Expand Down
17 changes: 17 additions & 0 deletions src/components/layout/Header.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { render, screen } from '@testing-library/react';
import { Header } from './Header';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('Header component', () => {
it('renders logo text and version', () => {
render(<Header />);
expect(screen.getByText('Isscope')).toBeInTheDocument();
expect(screen.getByText(/v\d+\.\d+\.\d+/)).toBeInTheDocument();
});

it('renders rightContent when provided', () => {
render(<Header rightContent={<button>Login</button>} />);
expect(screen.getByRole('button', { name: 'Login' })).toBeInTheDocument();
});
});
19 changes: 19 additions & 0 deletions src/components/layout/ScreenLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { render, screen } from '@testing-library/react';
import { ScreenLayout } from './ScreenLayout';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('ScreenLayout component', () => {
it('renders children correctly', () => {
render(<ScreenLayout>Content Here</ScreenLayout>);
expect(screen.getByText('Content Here')).toBeInTheDocument();
});

it('applies centered styles when centered prop is true', () => {
render(<ScreenLayout centered>Centered Content</ScreenLayout>);

// ScreenLayout wraps children in a div that gets centered styles
const element = screen.getByText('Centered Content');
expect(element).toHaveStyle({ alignItems: 'center', justifyContent: 'center' });
});
});
47 changes: 47 additions & 0 deletions src/components/layout/SplitPane.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { SplitPane } from './SplitPane';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('SplitPane component', () => {
it('renders left and right panes', () => {
render(<SplitPane left={<div>Left Pane</div>} right={<div>Right Pane</div>} />);
expect(screen.getByText('Left Pane')).toBeInTheDocument();
expect(screen.getByText('Right Pane')).toBeInTheDocument();
});

it('handles mouse events for dragging', () => {
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
Element.prototype.getBoundingClientRect = function () {
return {
width: 1000,
left: 0,
top: 0,
right: 1000,
bottom: 500,
x: 0,
y: 0,
height: 500,
toJSON: () => {},
};
};

render(
<SplitPane left={<div>Left Pane</div>} right={<div>Right Pane</div>} defaultSplit={40} />,
);

const leftPane = screen.getByText('Left Pane').parentElement;
const divider = screen.getByTestId('split-pane-divider');

expect(divider).toBeInTheDocument();
expect(leftPane).toHaveStyle({ width: '40%' });

fireEvent.mouseDown(divider);
fireEvent.mouseMove(window, { clientX: 600 });
fireEvent.mouseUp(window);

expect(leftPane).toHaveStyle({ width: '60%' });

Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
});
});
1 change: 1 addition & 0 deletions src/components/layout/SplitPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function SplitPane({
{/* Divider */}
<div
onMouseDown={handleMouseDown}
data-testid="split-pane-divider"
style={{
width: '1px',
background: 'var(--border)',
Expand Down
40 changes: 40 additions & 0 deletions src/components/terminal/TerminalInput.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { TerminalInput } from './TerminalInput';
import { describe, it, expect, vi } from 'vitest';
import React from 'react';

describe('TerminalInput component', () => {
it('renders correctly', () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
render(<TerminalInput value="" onChange={onChange} onSubmit={onSubmit} />);
expect(screen.getByPlaceholderText('Type here...')).toBeInTheDocument();
});

it('calls onChange when typing', () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
render(<TerminalInput value="" onChange={onChange} onSubmit={onSubmit} />);
const input = screen.getByPlaceholderText('Type here...');
fireEvent.change(input, { target: { value: 'hello' } });
expect(onChange).toHaveBeenCalledWith('hello');
});

it('calls onSubmit when Enter is pressed', () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
render(<TerminalInput value="test" onChange={onChange} onSubmit={onSubmit} />);
const input = screen.getByPlaceholderText('Type here...');
fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
expect(onSubmit).toHaveBeenCalled();
});

it('clears input when Escape is pressed', () => {
const onChange = vi.fn();
const onSubmit = vi.fn();
render(<TerminalInput value="test" onChange={onChange} onSubmit={onSubmit} />);
const input = screen.getByPlaceholderText('Type here...');
fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' });
expect(onChange).toHaveBeenCalledWith('');
});
});
16 changes: 16 additions & 0 deletions src/components/ui/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
import { describe, it, expect } from 'vitest';

describe('Button component', () => {
it('renders children correctly', () => {
render(<Button>Click Me</Button>);
expect(screen.getByText('Click Me')).toBeInTheDocument();
});

it('shows loading state when loading prop is true', () => {
render(<Button loading>Submit</Button>);
expect(screen.getByText('⏳')).toBeInTheDocument();
expect(screen.getByRole('button')).toBeDisabled();
});
});
24 changes: 24 additions & 0 deletions src/components/ui/Input.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { render, screen } from '@testing-library/react';
import { Input } from './Input';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('Input component', () => {
it('renders correctly', () => {
render(<Input placeholder="Enter text" />);
expect(screen.getByPlaceholderText('Enter text')).toBeInTheDocument();
});

it('passes ref correctly', () => {
const ref = React.createRef<HTMLInputElement>();
render(<Input ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLInputElement);
});

it('applies error styles when hasError is true', () => {
render(<Input hasError data-testid="error-input" />);
const input = screen.getByTestId('error-input');
// Directly checking style.border because JSDOM's toHaveStyle fails to parse shorthand properties containing CSS variables
expect(input.style.border).toContain('var(--status-error)');
});
});
25 changes: 25 additions & 0 deletions src/components/ui/MarkdownRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { render, screen } from '@testing-library/react';
import { MarkdownRenderer } from './MarkdownRenderer';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('MarkdownRenderer component', () => {
it('renders markdown content correctly', () => {
render(<MarkdownRenderer content="# Hello World" />);
// Testing specific output depends on react-markdown, we just test if the component renders the text
expect(screen.getByText('Hello World')).toBeInTheDocument();
});

it('renders code blocks correctly', () => {
render(<MarkdownRenderer content="`inline code`" />);
expect(screen.getByText('inline code')).toBeInTheDocument();
expect(screen.getByText('inline code').tagName).toBe('CODE');
});

it('renders links correctly', () => {
render(<MarkdownRenderer content="[GitHub](https://github.com)" />);
const link = screen.getByRole('link', { name: 'GitHub' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', 'https://github.com');
});
});
25 changes: 25 additions & 0 deletions src/components/ui/Panel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { render, screen } from '@testing-library/react';
import { Panel } from './Panel';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('Panel component', () => {
it('renders children correctly', () => {
render(<Panel>Panel Content</Panel>);
expect(screen.getByText('Panel Content')).toBeInTheDocument();
});

it('renders title when provided', () => {
render(<Panel title="Test Title">Content</Panel>);
expect(screen.getByText('[ Test Title ]')).toBeInTheDocument();
});

it('renders headerRight when provided', () => {
render(
<Panel title="Title" headerRight={<button>Action</button>}>
Content
</Panel>,
);
expect(screen.getByRole('button', { name: 'Action' })).toBeInTheDocument();
});
});
35 changes: 35 additions & 0 deletions src/components/ui/ProgressBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { render, screen } from '@testing-library/react';
import { ProgressBar } from './ProgressBar';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';

describe('ProgressBar component', () => {
beforeEach(() => {
vi.useFakeTimers();
});

afterEach(() => {
vi.useRealTimers();
});

it('renders correctly with current and total', () => {
render(<ProgressBar current={5} total={10} />);
expect(screen.getByText(/5\/10 \(50%\)/)).toBeInTheDocument();
});

it('renders label when provided', () => {
render(<ProgressBar current={5} total={10} label="Processing" />);
expect(screen.getByText('Processing')).toBeInTheDocument();
});

it('hides count when showCount is false', () => {
render(<ProgressBar current={5} total={10} showCount={false} />);
expect(screen.queryByText(/5\/10 \(50%\)/)).not.toBeInTheDocument();
});

it('shows ETA when startTime is provided', () => {
const startTime = Date.now() - 10000;
render(<ProgressBar current={5} total={10} startTime={startTime} />);
expect(screen.getByText(/ETA: 10s/)).toBeInTheDocument();
});
});
23 changes: 23 additions & 0 deletions src/components/ui/ScrollArea.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render, screen } from '@testing-library/react';
import { ScrollArea } from './ScrollArea';
import { describe, it, expect } from 'vitest';
import React from 'react';

describe('ScrollArea component', () => {
it('renders children correctly', () => {
render(<ScrollArea>Scrollable Content</ScrollArea>);
expect(screen.getByText('Scrollable Content')).toBeInTheDocument();
});

it('applies maxHeight when provided', () => {
render(<ScrollArea maxHeight="200px">Scroll Area Content</ScrollArea>);
const element = screen.getByText('Scroll Area Content');
expect(element.style.maxHeight).toBe('200px');
});

it('passes innerRef correctly', () => {
const ref = React.createRef<HTMLDivElement>();
render(<ScrollArea innerRef={ref}>Content</ScrollArea>);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});
1 change: 1 addition & 0 deletions src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
1 change: 1 addition & 0 deletions vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/// <reference types="vite/client" />
/// <reference types="@testing-library/jest-dom" />

interface ImportMetaEnv {
readonly VITE_GITHUB_TOKEN: string;
Expand Down
7 changes: 6 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineConfig } from 'vite';
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';
import path from 'path';
Expand All @@ -14,4 +14,9 @@ export default defineConfig({
port: 5173,
open: true,
},
test: {
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
globals: true,
},
});
Loading