Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
92 changes: 92 additions & 0 deletions frontend/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,95 @@ class ResizeObserver {
}

(global as any).ResizeObserver = ResizeObserver;

// --- Tauri Mocks ---

// Mock matchMedia
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});

// Mock Tauri Internals
(window as any).__TAURI_INTERNALS__ = {
invoke: jest.fn().mockResolvedValue(null),
transformCallback: jest.fn(),
metadata: {},
};

// Mock the module imports
jest.mock('@tauri-apps/api/core', () => ({
invoke: jest.fn().mockResolvedValue(null),
}));

jest.mock('@tauri-apps/api/app', () => ({
getVersion: jest.fn().mockResolvedValue('1.0.0'),
getName: jest.fn().mockResolvedValue('PictoPy'),
getTauriVersion: jest.fn().mockResolvedValue('2.0.0'),
}));

jest.mock('@tauri-apps/plugin-updater', () => ({
check: jest.fn().mockResolvedValue(null),
}));

jest.mock('@tauri-apps/plugin-dialog', () => ({
save: jest.fn().mockResolvedValue(null),
open: jest.fn().mockResolvedValue(null),
ask: jest.fn().mockResolvedValue(false),
}));

jest.mock('@tauri-apps/plugin-fs', () => ({
readDir: jest.fn().mockResolvedValue([]),
createDir: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('@tauri-apps/plugin-shell', () => ({
open: jest.fn().mockResolvedValue(undefined),
}));

jest.mock('@tauri-apps/plugin-store', () => ({
Store: jest.fn().mockImplementation(() => ({
get: jest.fn().mockResolvedValue(null),
set: jest.fn().mockResolvedValue(undefined),
save: jest.fn().mockResolvedValue(undefined),
load: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(true),
has: jest.fn().mockResolvedValue(false),
clear: jest.fn().mockResolvedValue(undefined),
keys: jest.fn().mockResolvedValue([]),
values: jest.fn().mockResolvedValue([]),
entries: jest.fn().mockResolvedValue([]),
length: jest.fn().mockResolvedValue(0),
onKeyChange: jest.fn().mockResolvedValue(() => {}), // Returns unlisten function
onChange: jest.fn().mockResolvedValue(() => {}), // Returns unlisten function
})),
}));

// Mock Axios
jest.mock('axios', () => {
const mockAxiosInstance = {
get: jest.fn().mockResolvedValue({ data: [] }),
post: jest.fn().mockResolvedValue({ data: {} }),
put: jest.fn().mockResolvedValue({ data: {} }),
patch: jest.fn().mockResolvedValue({ data: {} }),
delete: jest.fn().mockResolvedValue({ data: {} }),
interceptors: {
request: { use: jest.fn(), eject: jest.fn() },
response: { use: jest.fn(), eject: jest.fn() },
},
};
return {
default: mockAxiosInstance,
create: jest.fn(() => mockAxiosInstance),
...mockAxiosInstance,
};
});
7 changes: 4 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20",
"babel-jest": "^29.7.0",
"baseline-browser-mapping": "^2.9.19",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "^7.0.1",
Expand Down
24 changes: 13 additions & 11 deletions frontend/src/app/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { configureStore } from '@reduxjs/toolkit';
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import loaderReducer from '@/features/loaderSlice';
import onboardingReducer from '@/features/onboardingSlice';
import searchReducer from '@/features/searchSlice';
Expand All @@ -7,18 +7,20 @@ import faceClustersReducer from '@/features/faceClustersSlice';
import infoDialogReducer from '@/features/infoDialogSlice';
import folderReducer from '@/features/folderSlice';

export const rootReducer = combineReducers({
loader: loaderReducer,
onboarding: onboardingReducer,
images: imageReducer,
faceClusters: faceClustersReducer,
infoDialog: infoDialogReducer,
folders: folderReducer,
search: searchReducer,
});

export const store = configureStore({
reducer: {
loader: loaderReducer,
onboarding: onboardingReducer,
images: imageReducer,
faceClusters: faceClustersReducer,
infoDialog: infoDialogReducer,
folders: folderReducer,
search: searchReducer,
},
reducer: rootReducer,
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
// Inferred type: {loader: LoaderState, onboarding: OnboardingState, images: ImageState, ...}
export type AppDispatch = typeof store.dispatch;
175 changes: 175 additions & 0 deletions frontend/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { render, screen } from '@/test-utils';
import userEvent from '@testing-library/user-event';
import { Routes, Route, useLocation } from 'react-router';
import { AppSidebar } from '../Navigation/Sidebar/AppSidebar';
import { SidebarProvider } from '@/components/ui/sidebar';
import { ROUTES } from '@/constants/routes';

// Display current routes
const LocationDisplay = () => {
const location = useLocation();
return <div data-testid="location-display">{location.pathname}</div>;
};

// Sidebar + routes display
const SidebarWithRoutes = () => (
<SidebarProvider>
<AppSidebar />
<main>
<LocationDisplay />
<Routes>
<Route path="/" element={<div>Home Page</div>} />
<Route path={ROUTES.HOME} element={<div>Home Page</div>} />
<Route path={ROUTES.SETTINGS} element={<div>Settings Page</div>} />
<Route path={ROUTES.AI} element={<div>AI Tagging Page</div>} />
<Route path={ROUTES.FAVOURITES} element={<div>Favourites Page</div>} />
<Route path={ROUTES.VIDEOS} element={<div>Videos Page</div>} />
<Route path={ROUTES.ALBUMS} element={<div>Albums Page</div>} />
<Route path={ROUTES.MEMORIES} element={<div>Memories Page</div>} />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
</Routes>
</main>
</SidebarProvider>
);

describe('Sidebar', () => {
describe('Structure Tests', () => {
test('renders all main navigation links', () => {
render(
<SidebarProvider>
<AppSidebar />
</SidebarProvider>,
);

// Verify key navigation items exist
expect(screen.getByText('Home')).toBeInTheDocument();
expect(screen.getByText('AI Tagging')).toBeInTheDocument();
expect(screen.getByText('Favourites')).toBeInTheDocument();
expect(screen.getByText('Videos')).toBeInTheDocument();
expect(screen.getByText('Albums')).toBeInTheDocument();
expect(screen.getByText('Memories')).toBeInTheDocument();
expect(screen.getByText('Settings')).toBeInTheDocument();
});
});

describe('Navigation Interaction Tests', () => {
test('clicking Settings link navigates to /settings', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
); // start location

await user.click(screen.getByText('Settings')); // click settings

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.SETTINGS}`,
);
expect(screen.getByText('Settings Page')).toBeInTheDocument();
});

test('clicking Home link navigates to /home', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.SETTINGS}`] });

// start location
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.SETTINGS}`,
);

await user.click(screen.getByText('Home')); // click home

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);
expect(screen.getByText('Home Page')).toBeInTheDocument();
});

test('clicking AI Tagging link navigates to /ai-tagging', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);

await user.click(screen.getByText('AI Tagging')); // click ai-tagging

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.AI}`,
);
expect(screen.getByText('AI Tagging Page')).toBeInTheDocument();
});

test('clicking Favourites link navigates to /favourites', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);

await user.click(screen.getByText('Favourites')); // click favourites

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.FAVOURITES}`,
);
expect(screen.getByText('Favourites Page')).toBeInTheDocument();
});

test('clicking Videos link navigates to /videos', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);

await user.click(screen.getByText('Videos')); // click videos

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.VIDEOS}`,
);
expect(screen.getByText('Videos Page')).toBeInTheDocument();
});

test('clicking Albums link navigates to /albums', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);

await user.click(screen.getByText('Albums')); // click albums

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.ALBUMS}`,
);
expect(screen.getByText('Albums Page')).toBeInTheDocument();
});

test('clicking Memories link navigates to /memories', async () => {
const user = userEvent.setup();
render(<SidebarWithRoutes />, { initialRoutes: [`/${ROUTES.HOME}`] });

expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.HOME}`,
);

await user.click(screen.getByText('Memories')); // click memories

// verify
expect(screen.getByTestId('location-display')).toHaveTextContent(
`/${ROUTES.MEMORIES}`,
);
expect(screen.getByText('Memories Page')).toBeInTheDocument();
});
});
});
58 changes: 58 additions & 0 deletions frontend/src/components/__tests__/ThemeToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { render, screen } from '@/test-utils';
import userEvent from '@testing-library/user-event';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
import { ThemeSelector } from '../ThemeToggle';

describe('ThemeSelector', () => {
describe('Structure Tests', () => {
test('renders theme toggle button', () => {
render(<ThemeSelector />);

// button should be accessible
const button = screen.getByRole('button', { name: /themes/i });
expect(button).toBeInTheDocument();
});
});

describe('Interaction Tests', () => {
test('clicking toggle button opens theme dropdown', async () => {
const user = userEvent.setup();
render(<ThemeSelector />);

const button = screen.getByRole('button', { name: /themes/i });
await user.click(button); // open dropdown

// verify dropdown options are visible
expect(screen.getByText('Light')).toBeInTheDocument();
expect(screen.getByText('Dark')).toBeInTheDocument();
expect(screen.getByText('System')).toBeInTheDocument();
});

test('selecting Dark theme option closes dropdown', async () => {
const user = userEvent.setup();
render(<ThemeSelector />);

const button = screen.getByRole('button', { name: /themes/i });
await user.click(button); // open dropdown

const darkOption = screen.getByText('Dark');
await user.click(darkOption); // select dark

// dropdown should close (options no longer visible)
expect(screen.queryByText('Light')).not.toBeInTheDocument();
});

test('selecting Light theme option closes dropdown', async () => {
const user = userEvent.setup();
render(<ThemeSelector />);

const button = screen.getByRole('button', { name: /themes/i });
await user.click(button); // open dropdown

const lightOption = screen.getByText('Light');
await user.click(lightOption); // select light

// dropdown should close
expect(screen.queryByText('Dark')).not.toBeInTheDocument();
});
});
});
Loading