Skip to content
Merged
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
21 changes: 21 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default {
preset: 'ts-jest',
testEnvironment: 'jsdom',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
transform: {
'^.+\\.tsx?$': ['ts-jest', {
tsconfig: {
jsx: 'react',
esModuleInterop: true,
target: 'ES2017',
},
}],
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
maxWorkers: 1, // Workaround for Node.js < 18.14.0
};
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('@testing-library/jest-dom');
7,264 changes: 6,144 additions & 1,120 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"scripts": {
"build": "rsbuild build",
"dev": "rsbuild dev --open",
"preview": "rsbuild preview"
"preview": "rsbuild preview",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
Expand All @@ -19,12 +22,21 @@
"typescript": "^5.9.2"
},
"devDependencies": {
"@jest/globals": "^30.2.0",
"@rsbuild/core": "^1.5.8",
"@rsbuild/plugin-react": "^1.4.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"eslint": "^9.35.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"normalize.css": "^8.0.1",
"prettier": "^3.6.2"
"prettier": "^3.6.2",
"ts-jest": "^29.4.6"
}
}
2 changes: 1 addition & 1 deletion src/widgets/Weather.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export function Weather() {
});
}, []);

function getIcon(cloudCover: number, props?: IconProps): Icon {
function getIcon(cloudCover: number, props?: IconProps): React.ReactElement {
return cloudCover <= 100 * 0.25 ? (
<SunIcon {...props}></SunIcon>
) : cloudCover <= 100 * 0.625 ? (
Expand Down
124 changes: 124 additions & 0 deletions src/widgets/__tests__/BatteryWidget.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React from 'react';
import { render, screen, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BatteryWidget } from '../BatteryWidget';

describe('BatteryWidget', () => {
it('shows placeholder when battery API is not available', () => {
// Ensure getBattery is not available
const nav = navigator as any;
nav.getBattery = undefined;

render(<BatteryWidget />);

// Should show "—%" when API is not available
expect(screen.getByText('—%')).toBeInTheDocument();
});

it('renders battery icon SVG', () => {
const { container } = render(<BatteryWidget />);

// Check that SVG is rendered
const svg = container.querySelector('svg');
expect(svg).toBeInTheDocument();
expect(svg).toHaveAttribute('viewBox', '0 0 52 24');
});

it('displays battery percentage when API is available', async () => {
// Mock the Battery API
const mockBattery = {
level: 0.75, // 75%
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};

const nav = navigator as any;
nav.getBattery = jest.fn().mockResolvedValue(mockBattery);

render(<BatteryWidget />);

// Wait for the async battery API call to complete
await waitFor(() => {
expect(screen.getByText('75%')).toBeInTheDocument();
});
});

it('updates battery level when it changes', async () => {
let levelChangeCallback: (() => void) | null = null;

const mockBattery = {
level: 0.80,
addEventListener: jest.fn((event, callback) => {
if (event === 'levelchange') {
levelChangeCallback = callback;
}
}),
removeEventListener: jest.fn(),
};

const nav = navigator as any;
nav.getBattery = jest.fn().mockResolvedValue(mockBattery);

render(<BatteryWidget />);

// Wait for initial render
await waitFor(() => {
expect(screen.getByText('80%')).toBeInTheDocument();
});

// Simulate battery level change
mockBattery.level = 0.50;
if (levelChangeCallback) {
act(() => {
levelChangeCallback();
});
}

// Check that UI updated
await waitFor(() => {
expect(screen.getByText('50%')).toBeInTheDocument();
});
});

it('applies low battery styling when battery is below 20%', async () => {
const mockBattery = {
level: 0.15, // 15%
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};

const nav = navigator as any;
nav.getBattery = jest.fn().mockResolvedValue(mockBattery);

const { container } = render(<BatteryWidget />);

await waitFor(() => {
expect(screen.getByText('15%')).toBeInTheDocument();
});

// Check that the low class is applied
const row = container.querySelector('[class*="low"]');
expect(row).toBeInTheDocument();
});

it('applies ok styling when battery is above 20%', async () => {
const mockBattery = {
level: 0.85, // 85%
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
};

const nav = navigator as any;
nav.getBattery = jest.fn().mockResolvedValue(mockBattery);

const { container } = render(<BatteryWidget />);

await waitFor(() => {
expect(screen.getByText('85%')).toBeInTheDocument();
});

// Check that the ok class is applied
const row = container.querySelector('[class*="ok"]');
expect(row).toBeInTheDocument();
});
});
66 changes: 66 additions & 0 deletions src/widgets/__tests__/Clock.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Clock } from '../Clock';
import { WidgetState } from '../../Widget';

describe('Clock Widget', () => {
it('renders without crashing', () => {
const mockWidget: WidgetState<any> = {
id: 'test-123',
type: 'clock',
settings: {
use24HourClock: false,
showDate: true,
showYear: true,
},
size: { width: 2, height: 2 },
position: { gridX: 0, gridY: 0 },
};

render(<Clock {...mockWidget} />);

// Check that time is rendered (should contain a colon for hours:minutes)
expect(screen.getByText(/:/)).toBeInTheDocument();
});

it('displays date when showDate is true', () => {
const mockWidget: WidgetState<any> = {
id: 'test-123',
type: 'clock',
settings: {
use24HourClock: false,
showDate: true,
showYear: false,
},
size: { width: 2, height: 2 },
position: { gridX: 0, gridY: 0 },
};

render(<Clock {...mockWidget} />);

// Check if a day of the week is rendered (Monday, Tuesday, etc.)
const dayPattern = /Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/;
expect(screen.getByText(dayPattern)).toBeInTheDocument();
});

it('hides date when showDate is false', () => {
const mockWidget: WidgetState<any> = {
id: 'test-123',
type: 'clock',
settings: {
use24HourClock: false,
showDate: false,
showYear: false,
},
size: { width: 2, height: 2 },
position: { gridX: 0, gridY: 0 },
};

render(<Clock {...mockWidget} />);

// Check that no day of the week is rendered
const dayPattern = /Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday/;
expect(screen.queryByText(dayPattern)).not.toBeInTheDocument();
});
});
61 changes: 61 additions & 0 deletions src/widgets/__tests__/Notepad.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Notepad } from '../Notepad';

describe('Notepad Widget', () => {
beforeEach(() => {
// Clear localStorage before each test
localStorage.clear();
});

it('renders notepad with title', () => {
render(<Notepad />);

// Check that the title is present
expect(screen.getByText('My Notes')).toBeInTheDocument();
});

it('renders textarea with placeholder', () => {
render(<Notepad />);

// Check that textarea is present
expect(screen.getByPlaceholderText('Type your notes here...')).toBeInTheDocument();
});

it('allows typing in the textarea', () => {
render(<Notepad />);

const textarea = screen.getByPlaceholderText('Type your notes here...') as HTMLTextAreaElement;

// Type in the textarea
fireEvent.change(textarea, { target: { value: 'My test note' } });

// Check that the value was updated
expect(textarea.value).toBe('My test note');
});

it('saves note to localStorage when typing', () => {
render(<Notepad />);

const textarea = screen.getByPlaceholderText('Type your notes here...') as HTMLTextAreaElement;

// Type in the textarea
fireEvent.change(textarea, { target: { value: 'Remember to test!' } });

// Check that localStorage was updated
expect(localStorage.getItem('notepad-note')).toBe('Remember to test!');
});

it('loads saved note from localStorage on mount', () => {
// Pre-populate localStorage
localStorage.setItem('notepad-note', 'Previously saved note');

render(<Notepad />);

const textarea = screen.getByPlaceholderText('Type your notes here...') as HTMLTextAreaElement;

// Check that the saved note was loaded
expect(textarea.value).toBe('Previously saved note');
});
});
Loading