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
1,304 changes: 1,262 additions & 42 deletions webdev-prosjekt1/package-lock.json

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions webdev-prosjekt1/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@tanstack/query-sync-storage-persister": "^5.102.8",
"@phosphor-icons/react": "^2.1.10",
"@tanstack/query-sync-storage-persister": "^5.102.8",
"@tanstack/react-query": "^5.102.8",
"@tanstack/react-query-persist-client": "^5.102.8",
"react": "^19.2.8",
Expand All @@ -21,6 +23,10 @@
"@babel/core": "^7.29.7",
"@eslint/js": "^10.0.1",
"@rolldown/plugin-babel": "^0.2.3",
"@testing-library/dom": "^10.4.2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/babel__core": "^7.20.5",
"@types/node": "^24.13.3",
"@types/react": "^19.2.18",
Expand All @@ -31,8 +37,10 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.4",
"globals": "^17.11.0",
"jsdom": "^27.1.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.67.0",
"vite": "^8.3.0"
"vite": "^8.3.0",
"vitest": "^4.0.18"
}
}
1 change: 0 additions & 1 deletion webdev-prosjekt1/src/components/AlbumDetailPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
aspect-ratio: 1;
border: 1px solid var(--border_gold);
background: var(--surface_dark_transparent);
box-shadow: 18px 22px 0 var(--shadow_gold), 0 24px 50px var(--shadow_black);
}

.album-detail-page .detail-art::after {
Expand Down
7 changes: 1 addition & 6 deletions webdev-prosjekt1/src/components/ArtistDetailPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,10 @@
border-radius: 50%;
overflow: hidden;
background: var(--surface_dark_transparent);
box-shadow: 18px 22px 0 var(--shadow_gold), 0 24px 50px var(--shadow_black);
}

.artist-detail-page .detail-art::after {
content: '';
position: absolute;
inset: 13px;
border: 1px solid var(--border_gold_inner);
border-radius: 50%;
display: none;
}

.artist-detail-page .detail-art img {
Expand Down
1 change: 1 addition & 0 deletions webdev-prosjekt1/src/components/PreviewPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export const PreviewPlayer = ({ tracks, spotifyUri, onTrackChange }: PreviewPlay
title={isPlaying ? 'Pause preview' : 'Play preview'}
>
<span className={isPlaying ? 'preview-pause-icon' : 'preview-play-icon'} aria-hidden='true' />
<span>Preview</span>
</button>
{spotifyUri && (
<a className='spotify-link' href={spotifyUriToWebUrl(spotifyUri)} target='_blank' rel='noreferrer'>
Expand Down
10 changes: 6 additions & 4 deletions webdev-prosjekt1/src/components/TrackDetailPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
aspect-ratio: 1;
border: 1px solid var(--border_gold);
background: var(--surface_dark_transparent);
box-shadow: 18px 22px 0 var(--shadow_gold), 0 24px 50px var(--shadow_black);
}

.detail-art::after {
Expand Down Expand Up @@ -124,10 +123,13 @@

.preview-player-button {
display: grid;
place-items: center;
width: 44px;
grid-template-columns: auto auto;
align-items: center;
justify-content: center;
gap: 10px;
min-width: 132px;
height: 44px;
padding: 0;
padding: 0 18px;
box-sizing: border-box;
border: 1px solid var(--primary_color);
border-radius: 999px;
Expand Down
33 changes: 33 additions & 0 deletions webdev-prosjekt1/src/test/FavButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, beforeEach } from 'vitest';
import { FavButton } from '../components/FavButton';
import { FavoritesProvider } from '../hooks/useFavorites';
import type { AlbumTrack } from '../api/types';

const track: AlbumTrack = {
id: 'favorite-track',
name: 'Favorite track',
type: 'track',
href: '',
artists: [{ id: 1, name: 'Artist', type: 'artist', href: '', uri: '' }],
duration_ms: 180000,
track_number: 1,
uri: 'spotify:track:favorite-track',
};

describe('FavButton', () => {
beforeEach(() => localStorage.clear());

it('adds and removes a favorite while persisting the change', async () => {
const user = userEvent.setup();
render(<FavoritesProvider><FavButton track={track} /></FavoritesProvider>);
const button = screen.getByRole('button');

await user.click(button);
expect(JSON.parse(localStorage.getItem('favorites') ?? '[]')).toEqual([track]);

await user.click(button);
expect(JSON.parse(localStorage.getItem('favorites') ?? '[]')).toEqual([]);
});
});
49 changes: 49 additions & 0 deletions webdev-prosjekt1/src/test/FilterPopup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { cleanup, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { EMPTY_FILTERS, FilterPopup, type Filters } from '../components/FilterPopup';

const renderFilterPopup = (onClose = vi.fn(), onClear = vi.fn()) => {
const onChange = vi.fn();
const Wrapper = () => {
const [filters, setFilters] = useState<Filters>(EMPTY_FILTERS);
return <FilterPopup filters={filters} onChange={(nextFilters) => { setFilters(nextFilters); onChange(nextFilters); }} onClear={onClear} onClose={onClose} />;
};

return { ...render(<Wrapper />), onChange };
};

describe('FilterPopup', () => {
afterEach(cleanup);

it('updates text, toggle, and genre filters', async () => {
const user = userEvent.setup();
const { onChange } = renderFilterPopup();

await user.type(screen.getByPlaceholderText('Artist name'), 'Kygo');
await user.click(screen.getByText('New releases only'));
await user.click(screen.getByRole('button', { name: 'pop' }));

expect(onChange).toHaveBeenLastCalledWith({
...EMPTY_FILTERS,
artist: 'Kygo',
onlyNew: true,
genres: ['pop'],
});
});

it('clears filters and closes from the overlay or close button', async () => {
const user = userEvent.setup();
const onClose = vi.fn();
const onClear = vi.fn();
const { container } = renderFilterPopup(onClose, onClear);

await user.click(screen.getByRole('button', { name: 'Clear' }));
await user.click(screen.getByRole('button', { name: 'Close filters' }));
await user.click(container.querySelector('.filter-overlay') as HTMLElement);

expect(onClear).toHaveBeenCalledOnce();
expect(onClose).toHaveBeenCalledTimes(2);
});
});
43 changes: 43 additions & 0 deletions webdev-prosjekt1/src/test/PreviewPlayer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { PreviewPlayer } from '../components/PreviewPlayer';

vi.mock('../api/itunes', () => ({
fetchPreview: vi.fn().mockResolvedValue('https://cdn.example/preview.mp3'),
}));

const track = {
id: 'preview-track',
name: 'Preview track',
artists: [{ name: 'Preview artist' }],
};

describe('PreviewPlayer', () => {
beforeAll(() => {
Object.defineProperty(HTMLMediaElement.prototype, 'play', {
configurable: true,
value: vi.fn().mockResolvedValue(undefined),
});
Object.defineProperty(HTMLMediaElement.prototype, 'pause', {
configurable: true,
value: vi.fn(),
});
});

it('plays and pauses a mocked preview and exposes the Spotify link', async () => {
const user = userEvent.setup();
const onTrackChange = vi.fn();

render(<PreviewPlayer tracks={[track]} spotifyUri='spotify:track:preview-track' onTrackChange={onTrackChange} />);
const button = screen.getByRole('button', { name: 'Play preview' });

expect(screen.getByRole('link', { name: 'Open in Spotify' })).toHaveAttribute('href', 'https://open.spotify.com/track/preview-track');
await user.click(button);
await waitFor(() => expect(screen.getByRole('button', { name: 'Pause preview' })).toBeEnabled());
expect(onTrackChange).toHaveBeenCalledWith('preview-track');

await user.click(screen.getByRole('button', { name: 'Pause preview' }));
expect(onTrackChange).toHaveBeenLastCalledWith(null);
});
});
28 changes: 28 additions & 0 deletions webdev-prosjekt1/src/test/Searchbar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { Searchbar } from '../components/Searchbar';

describe('Searchbar', () => {
it('submits trimmed input and clears it through user interaction', async () => {
const user = userEvent.setup();
const onSearch = vi.fn();

render(<Searchbar onSearch={onSearch} />);
const input = screen.getByRole('searchbox');

await user.type(input, ' Daft Punk ');
await user.keyboard('{Enter}');
expect(onSearch).toHaveBeenCalledWith('Daft Punk');

await user.click(screen.getByRole('button', { name: 'Clear search' }));
expect(input).toHaveValue('');
expect(onSearch).toHaveBeenLastCalledWith('');
});

it('renders a stable snapshot from its initial prop', () => {
const { container } = render(<Searchbar onSearch={vi.fn()} initialQuery='Muse' />);

expect(container.firstChild).toMatchSnapshot();
});
});
46 changes: 46 additions & 0 deletions webdev-prosjekt1/src/test/TrackCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';
import { TrackCard } from '../components/TrackCard';
import type { Track } from '../api/types';

vi.mock('../hooks/useFavorites', () => ({
useFavorites: () => ({
favorites: [],
addFavorite: vi.fn(),
removeFavorite: vi.fn(),
}),
}));

const track: Track = {
id: 'track-1',
name: 'Harder Better Faster Stronger',
type: 'track',
href: 'https://api.spotify.com/v1/tracks/track-1',
artists: [{ id: 1, name: 'Daft Punk', type: 'artist', href: '', uri: '' }],
album: {
id: 'album-1', name: 'Discovery', album_type: 'album', href: '', artists: [], images: [],
release_date: '2001', release_date_precision: 'year', total_tracks: 14, uri: '',
},
duration_ms: 224000,
explicit: false,
track_number: 4,
external_ids: { isrc: 'TRACK-1' },
is_playable: true,
uri: 'spotify:track:track-1',
};

describe('TrackCard', () => {
it('uses the active prop and calls its click handler', async () => {
const user = userEvent.setup();
const onClick = vi.fn();

const { container } = render(<TrackCard track={track} isActive onClick={onClick} />);
const card = container.querySelector('li');
expect(card).not.toBeNull();

expect(card).toHaveClass('is-active');
await user.click(card as HTMLElement);
expect(onClick).toHaveBeenCalledOnce();
});
});
39 changes: 39 additions & 0 deletions webdev-prosjekt1/src/test/__snapshots__/Searchbar.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`Searchbar > renders a stable snapshot from its initial prop 1`] = `
<form
class="searchbar"
>
<svg
aria-hidden="true"
class="search-icon"
fill="currentColor"
height="22"
viewBox="0 0 256 256"
width="22"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"
/>
</svg>
<input
aria-label="Search music"
placeholder="Search artists, albums or tracks"
type="search"
value="Muse"
/>
<button
aria-label="Clear search"
class="search-clear-button"
title="Clear search"
type="button"
>
<span
aria-hidden="true"
>
×
</span>
</button>
</form>
`;
1 change: 1 addition & 0 deletions webdev-prosjekt1/src/test/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';
Loading