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
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,18 @@
},
"dependencies": {
"@biomejs/biome": "1.9.4",
"@reduxjs/toolkit": "^2.8.2",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"axios": "^1.9.0",
"fast-average-color": "^9.5.0",
"qs": "^6.14.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^6.27.0",
"sass": "^1.89.2",
"typescript": "^5.8.2",
"universal-cookie": "^7.2.2"
"universal-cookie": "^7.2.2",
"zustand": "^5.0.5"
},
"browserslist": {
"production": [
Expand Down
14 changes: 5 additions & 9 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@ import Player from './components/Player/Player';
import SideBar from './components/SideBar/SideBar';
import PlaylistDetail from './pages/PlaylistDetail/PlaylistDetail';
import Playlists from './pages/Playlists/Playlists';
import { useAppDispatch, useAppSelector } from './store/hooks';
import {
fetchUserPlaylists,
selectUserPlaylists,
} from './store/reducers/userPlaylists.slice';
import { useAppStore, useUserPlaylists } from './store/zustand-store';

const App = () => {
const cookies = new Cookies();
const dispatch = useAppDispatch();
const fetchUserPlaylists = useAppStore((state) => state.fetchUserPlaylists);
const user = 'smedjan';
const { playlists, loading, error } = useAppSelector(selectUserPlaylists);
const { playlists, loading, error } = useUserPlaylists();

useEffect(() => {
dispatch(fetchUserPlaylists(user));
}, [dispatch]);
fetchUserPlaylists(user);
}, [fetchUserPlaylists]);

return (
<>
Expand Down
60 changes: 38 additions & 22 deletions src/components/Player/Player.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { describe, expect, test } from 'vitest';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { mockTrack } from '../../../tests/mockData';
import { createMockStore } from '../../store/store';
import { createTestStore, setupTestStoreMocks } from '../../store/test-utils';
import * as zustandStore from '../../store/zustand-store';
import Player from './Player';

const playerStore = createMockStore({
currentSong: { song: null, playing: false },
});

const playerStoreWithSong = createMockStore({
currentSong: { song: mockTrack, playing: true },
});
vi.mock('../../store/zustand-store');

describe('Player', () => {
test('should render nothing', async () => {
render(
<Provider store={playerStore}>
<Player />
</Provider>,
);
beforeEach(() => {
vi.clearAllMocks();
});

test('should render nothing when no song is loaded', async () => {
const testStore = createTestStore({
currentSong: {
song: null,
playing: false,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);
render(<Player />);
expect(screen.queryByTestId('audioEml')).toBeFalsy();
});

test('should render audio element', async () => {
render(
<Provider store={playerStoreWithSong}>
<Player />
</Provider>,
);
test('should render audio element when song is loaded and playing', async () => {
const testStore = createTestStore({
currentSong: {
song: mockTrack,
playing: true,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);
render(<Player />);
expect(screen.getByTestId('audioEml')).toBeTruthy();
});

test('should render audio element when song is loaded but not playing', async () => {
const testStore = createTestStore({
currentSong: {
song: mockTrack,
playing: false,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);
render(<Player />);
expect(screen.getByTestId('audioEml')).toBeTruthy();
});
});
14 changes: 5 additions & 9 deletions src/components/Player/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ import Pause from '../../assets/pause.svg?react';
import Play from '../../assets/play.svg?react';
import Volume from '../../assets/volume.svg?react';
import VolumeMuted from '../../assets/volumeMuted.svg?react';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import {
playPause,
selectCurrentSong,
} from '../../store/reducers/currentSong.slice';
import { useAppStore, useCurrentSong } from '../../store/zustand-store';
import msToMinutesAndSeconds from '../../utils/msToMinutes';
import useBar from '../../utils/useBar';
import useStopwatch from '../../utils/useStopwatch';
import styles from './Player.module.scss';

const Player = () => {
const dispatch = useAppDispatch();
const { song, playing } = useAppSelector(selectCurrentSong);
const playPause = useAppStore((state) => state.playPause);
const { song, playing } = useCurrentSong();
const audioEml = useRef<ElementRef<'audio'>>(null);

const timeRef = useRef<ElementRef<'div'>>(null);
Expand Down Expand Up @@ -97,15 +93,15 @@ const Player = () => {
>
<track kind="captions" />
</audio>
<button type="button" onClick={() => dispatch(playPause())}>
<button type="button" onClick={() => playPause()}>
{playing ? <Pause /> : <Play />}
</button>
<div className={styles.BarContainer}>
<div>{msToMinutesAndSeconds(currentTime)}</div>
<div
className={styles.Wrapper}
onClick={(event) => barCallBack(event, timeRef, setProgress)}
onKeyDown={() => dispatch(playPause())}
onKeyDown={() => playPause()}
// biome-ignore lint/a11y/useSemanticElements: clickable div is fine
role="button"
tabIndex={0}
Expand Down
6 changes: 1 addition & 5 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux';
import App from './App';
import './index.scss';
import { store } from './store/store';

const container = document.getElementById('root');
// biome-ignore lint/style/noNonNullAssertion: <explanation>
const root = createRoot(container!);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
<App />
</React.StrictMode>,
);
98 changes: 63 additions & 35 deletions src/pages/PlaylistDetail/PlaylistDetail.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,82 @@
import { cleanup, render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { afterEach, describe, expect, test } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { mockPlaylistDetails } from '../../../tests/mockData';
import { createMockStore } from '../../store/store';
import { createTestStore, setupTestStoreMocks } from '../../store/test-utils';
import * as zustandStore from '../../store/zustand-store';
import PlaylistDetail from './PlaylistDetail';

const store = createMockStore({
playlistDetail: {
playlist: mockPlaylistDetails,
loading: false,
error: '',
},
});

const loadingStore = createMockStore({
playlistDetail: {
playlist: null,
loading: true,
error: '',
},
});
vi.mock('../../store/zustand-store');

describe('Playlist details', () => {
afterEach(cleanup);
afterEach(() => {
cleanup();
vi.clearAllMocks();
});

test('should render loader when playlist is loading', async () => {
const testStore = createTestStore({
playlistDetail: {
playlist: null,
loading: true,
error: '',
},
currentSong: {
song: null,
playing: false,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);

test('should render loader', async () => {
render(
<Provider store={loadingStore}>
<MemoryRouter initialEntries={['/playlist/123']}>
<PlaylistDetail />
</Provider>,
</MemoryRouter>,
);
expect(screen.getByRole('progressbar', { busy: true })).toBeTruthy();
});

test('should render playlist details', async () => {
test('should render playlist details when loaded', async () => {
const testStore = createTestStore({
playlistDetail: {
playlist: mockPlaylistDetails,
loading: false,
error: '',
},
currentSong: {
song: null,
playing: false,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);
render(
<Provider store={store}>
<MemoryRouter initialEntries={['/playlist/123']}>
<PlaylistDetail />
</Provider>,
</MemoryRouter>,
);
expect(screen.findByText('Hits du Moment')).toBeTruthy();
expect(await screen.findByText('Hits du Moment')).toBeTruthy();
});

// test('should have a background color', async () => {
// render(
// <Provider store={store}>
// <PlaylistDetail />
// </Provider>,
// );

// expect(screen.getByTestId('Background').style.backgroundColor).toBeTruthy();
// });
test('should render error state when there is an error', async () => {
const testStore = createTestStore({
playlistDetail: {
playlist: null,
loading: false,
error: 'Failed to load playlist',
},
currentSong: {
song: null,
playing: false,
},
});
setupTestStoreMocks(testStore, zustandStore, vi);
render(
<MemoryRouter initialEntries={['/playlist/123']}>
<PlaylistDetail />
</MemoryRouter>,
);
// The error handling would depend on how the component handles error states
// For now, we just verify the component renders without crashing
expect(screen.queryByRole('progressbar', { busy: true })).toBeFalsy();
});
});
19 changes: 9 additions & 10 deletions src/pages/PlaylistDetail/PlaylistDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,27 @@ import { useParams } from 'react-router-dom';
import Time from '../../assets/time.svg?react';
import Loader from '../../components/Loader/Loader';
import NotFound from '../../components/NotFound/NotFound';
import { useAppDispatch, useAppSelector } from '../../store/hooks';
import { selectCurrentSong } from '../../store/reducers/currentSong.slice';
import {
fetchPlaylistById,
playlistDetailsSelector,
} from '../../store/reducers/playlistDetail.slice';
useAppStore,
useCurrentSong,
usePlaylistDetail,
} from '../../store/zustand-store';
import msToMinutesAndSeconds from '../../utils/msToMinutes';
import styles from './PlaylistDetail.module.scss';
import SongItem from './SongItem/SongItem';

const PlaylistDetail = () => {
const { id } = useParams<{ id: string }>();
const coverRef = useRef<HTMLImageElement | null>(null);
const dispatch = useAppDispatch();
const { playlist, loading, error } = useAppSelector(playlistDetailsSelector);
const { song } = useAppSelector(selectCurrentSong);
const fetchPlaylistById = useAppStore((state) => state.fetchPlaylistById);
const { playlist, loading, error } = usePlaylistDetail();
const { song } = useCurrentSong();

useEffect(() => {
if (id != null) {
void dispatch(fetchPlaylistById(id));
void fetchPlaylistById(id);
}
}, [id, dispatch]);
}, [id, fetchPlaylistById]);

useEffect(() => {
if (coverRef.current != null) {
Expand Down
7 changes: 3 additions & 4 deletions src/pages/PlaylistDetail/SongItem/SongItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Play from '../../../assets/play.svg?react';
import { useAppDispatch } from '../../../store/hooks';
import { loadSong } from '../../../store/reducers/currentSong.slice';
import { useAppStore } from '../../../store/zustand-store';
import type { Track } from '../../../types/track.interface';
import formatDate from '../../../utils/formatDate';
import msToMinutesAndSeconds from '../../../utils/msToMinutes';
Expand All @@ -13,12 +12,12 @@ interface SongItemPros {
}

const SongItem = ({ song, index, current }: SongItemPros) => {
const dispatch = useAppDispatch();
const loadSong = useAppStore((state) => state.loadSong);
const previewAvailable = song.track?.preview_url !== null;

const handleSongClick = (): void => {
if (previewAvailable) {
dispatch(loadSong(song));
loadSong(song);
}
};

Expand Down
10 changes: 0 additions & 10 deletions src/store/hooks.ts

This file was deleted.

Loading
Loading