Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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>,
);
96 changes: 62 additions & 34 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();
});

// 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();
});
});
17 changes: 6 additions & 11 deletions src/pages/PlaylistDetail/PlaylistDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,23 @@ 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';
import { 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