diff --git a/src/components/mobile-bottom-nav.tsx b/src/components/mobile-bottom-nav.tsx
index 1e9f8c5..47119da 100644
--- a/src/components/mobile-bottom-nav.tsx
+++ b/src/components/mobile-bottom-nav.tsx
@@ -28,14 +28,17 @@ export function MobileBottomNav() {
)}
>
{icon}
-
{label}
+
+ {label}
+
))}
diff --git a/src/components/movie-row.tsx b/src/components/movie-row.tsx
index 9d86399..8c95374 100644
--- a/src/components/movie-row.tsx
+++ b/src/components/movie-row.tsx
@@ -9,7 +9,7 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
-import type { Movie } from "@/types";
+import type { Movie, StreamingProvider } from "@/types";
import { formatRuntime } from "@/lib/utils";
const SEERR_LABEL: Record
= {
@@ -32,7 +32,7 @@ interface MovieRowProps {
movie: Movie;
position: number;
seerrUrl?: string | null;
- streamingProviders: { providerId: number; providerName: string }[];
+ streamingProviders: StreamingProvider[];
streamingLink: string | null;
onMarkWatched: (movie: Movie) => void;
onForceDownload: (movieId: number) => void;
@@ -136,7 +136,7 @@ export function MovieRow({
href={streamingLink}
target="_blank"
rel="noopener noreferrer"
- className="rounded border border-stone-600 bg-stone-800 text-white px-2 py-0.5 text-xs font-medium hover:bg-stone-700 transition-colors"
+ className="rounded border border-amber-400 bg-white text-amber-700 px-2 py-0.5 text-xs font-medium hover:bg-amber-50 transition-colors"
>
Watch ↗
diff --git a/src/components/sidebar-utils.tsx b/src/components/sidebar-utils.tsx
index e7fde0e..c2e3ddd 100644
--- a/src/components/sidebar-utils.tsx
+++ b/src/components/sidebar-utils.tsx
@@ -17,11 +17,11 @@ export function PlexSyncButton() {
}
}
- const label =
- state === 'loading' ? '⏳ Syncing…'
- : state === 'ok' ? '✅ Synced!'
- : state === 'error' ? '❌ Failed'
- : '🎭 Sync Plex'
+ const content: { icon: string; text: string } =
+ state === 'loading' ? { icon: '⏳', text: 'Syncing…' }
+ : state === 'ok' ? { icon: '✅', text: 'Synced!' }
+ : state === 'error' ? { icon: '❌', text: 'Failed' }
+ : { icon: '🎭', text: 'Sync Plex' }
return (
)
}
@@ -54,11 +55,11 @@ export function StreamingRefreshButton() {
}
}
- const label =
- state === 'loading' ? '⏳ Refreshing…'
- : state === 'ok' ? '✅ Refreshed!'
- : state === 'error' ? '❌ Failed'
- : '📡 Refresh Streaming'
+ const content: { icon: string; text: string } =
+ state === 'loading' ? { icon: '⏳', text: 'Refreshing…' }
+ : state === 'ok' ? { icon: '✅', text: 'Refreshed!' }
+ : state === 'error' ? { icon: '❌', text: 'Failed' }
+ : { icon: '📡', text: 'Refresh Streaming' }
return (
)
}
@@ -93,7 +95,7 @@ export function AskClaudeLink() {
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 text-xs text-amber-700 hover:bg-amber-100 rounded-lg transition-colors"
>
- ✨ Ask Claude
+ ✨ Ask Claude
)
}
diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx
index c3b3938..fbf6763 100644
--- a/src/components/sidebar.tsx
+++ b/src/components/sidebar.tsx
@@ -20,7 +20,7 @@ export function Sidebar() {
{/* Logo */}
@@ -38,7 +38,7 @@ export function Sidebar() {
: 'text-amber-800 hover:bg-amber-100'
)}
>
- {icon}
+ {icon}
{label}
))}
@@ -46,22 +46,6 @@ export function Sidebar() {
{/* Utility links */}
diff --git a/tests/add-page.test.tsx b/tests/add-page.test.tsx
new file mode 100644
index 0000000..e5706fd
--- /dev/null
+++ b/tests/add-page.test.tsx
@@ -0,0 +1,25 @@
+// tests/add-page.test.tsx
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+
+vi.mock('next/navigation', () => ({
+ useRouter: () => ({ push: vi.fn() }),
+}))
+
+vi.stubGlobal('fetch', vi.fn())
+
+import AddMoviePage from '@/app/add/page'
+
+describe('AddMoviePage', () => {
+ it('shows Browse Criterion and Browse IMDB helper links', () => {
+ render()
+ expect(screen.getByRole('link', { name: /browse criterion/i })).toHaveAttribute(
+ 'href',
+ 'https://www.criterion.com/shop/browse/list?q=&format=all'
+ )
+ expect(screen.getByRole('link', { name: /browse imdb/i })).toHaveAttribute(
+ 'href',
+ 'https://www.imdb.com/search/title/?title_type=feature'
+ )
+ })
+})
diff --git a/tests/mobile-bottom-nav.test.tsx b/tests/mobile-bottom-nav.test.tsx
index 4075779..4bed0cd 100644
--- a/tests/mobile-bottom-nav.test.tsx
+++ b/tests/mobile-bottom-nav.test.tsx
@@ -32,16 +32,30 @@ describe('MobileBottomNav', () => {
it('highlights the active tab', () => {
render()
- // usePathname returns '/watchlist' — the List link should have the active colour
const listLink = screen.getByRole('link', { name: /list/i })
expect(listLink).toHaveClass('text-amber-600')
- // Active icon span gets the background pill
const iconSpan = listLink.querySelector('span')
- expect(iconSpan).toHaveClass('bg-amber-100')
- // Inactive tabs should not have the active colour or pill
+ expect(iconSpan).toHaveClass('bg-amber-600')
const watchedLink = screen.getByRole('link', { name: /watched/i })
expect(watchedLink).not.toHaveClass('text-amber-600')
const inactiveIconSpan = watchedLink.querySelector('span')
- expect(inactiveIconSpan).not.toHaveClass('bg-amber-100')
+ expect(inactiveIconSpan).not.toHaveClass('bg-amber-600')
+ })
+
+ it('applies bold font weight to the active tab label', () => {
+ render()
+ const listLink = screen.getByRole('link', { name: /list/i })
+ const spans = listLink.querySelectorAll('span')
+ const labelSpan = spans[spans.length - 1]
+ expect(labelSpan).toHaveClass('font-bold')
+ })
+
+ it('wraps tab emoji icons with aria-hidden', () => {
+ render()
+ const links = screen.getAllByRole('link')
+ links.forEach((link) => {
+ const iconSpan = link.querySelector('span[aria-hidden="true"]')
+ expect(iconSpan).not.toBeNull()
+ })
})
})
diff --git a/tests/mobile-header.test.tsx b/tests/mobile-header.test.tsx
index a2744ed..acd39cd 100644
--- a/tests/mobile-header.test.tsx
+++ b/tests/mobile-header.test.tsx
@@ -27,8 +27,8 @@ describe('MobileHeader', () => {
await waitFor(() => {
expect(screen.getByText('Browse Criterion')).toBeInTheDocument()
expect(screen.getByText('Browse IMDB')).toBeInTheDocument()
- expect(screen.getByText('🎭 Sync Plex')).toBeInTheDocument()
- expect(screen.getByText('✨ Ask Claude')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /sync plex/i })).toBeInTheDocument()
+ expect(screen.getByText(/ask claude/i)).toBeInTheDocument()
expect(screen.getByRole('link', { name: /settings/i })).toBeInTheDocument()
})
})
diff --git a/tests/movie-row.test.tsx b/tests/movie-row.test.tsx
index dc3ffae..91f245e 100644
--- a/tests/movie-row.test.tsx
+++ b/tests/movie-row.test.tsx
@@ -2,7 +2,7 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { MovieRow } from '@/components/movie-row'
-import type { Movie, User } from '@/types'
+import type { Movie } from '@/types'
vi.mock('next/image', () => ({
default: ({ src, alt }: { src: string; alt: string }) =>
,
@@ -101,4 +101,18 @@ describe('MovieRow layout', () => {
const infoSection = screen.getByText('Jeanne Dielman').closest('div')
expect(infoSection).toContainElement(screen.getByText('Queued'))
})
+
+ it('renders Watch link with amber outline instead of dark stone', () => {
+ render(
+
+ )
+ const watchLink = screen.getByRole('link', { name: /watch/i })
+ expect(watchLink).not.toHaveClass('bg-stone-800')
+ expect(watchLink).toHaveClass('border-amber-400')
+ })
})
diff --git a/tests/sidebar-utils.test.tsx b/tests/sidebar-utils.test.tsx
index 82666fa..d268f66 100644
--- a/tests/sidebar-utils.test.tsx
+++ b/tests/sidebar-utils.test.tsx
@@ -14,7 +14,7 @@ describe('PlexSyncButton', () => {
it('renders in idle state', () => {
render()
- expect(screen.getByText('🎭 Sync Plex')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /sync plex/i })).toBeInTheDocument()
})
})
@@ -23,21 +23,21 @@ describe('StreamingRefreshButton', () => {
it('renders in idle state', () => {
render()
- expect(screen.getByText('📡 Refresh Streaming')).toBeInTheDocument()
+ expect(screen.getByRole('button', { name: /refresh streaming/i })).toBeInTheDocument()
})
it('posts to /api/streaming-providers/refresh on click', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({}) } as any)
render()
- fireEvent.click(screen.getByText('📡 Refresh Streaming'))
+ fireEvent.click(screen.getByRole('button', { name: /refresh streaming/i }))
expect(global.fetch).toHaveBeenCalledWith('/api/streaming-providers/refresh', { method: 'POST' })
})
it('shows success state after refresh completes', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, json: async () => ({}) } as any)
render()
- fireEvent.click(screen.getByText('📡 Refresh Streaming'))
- await waitFor(() => expect(screen.getByText('✅ Refreshed!')).toBeInTheDocument())
+ fireEvent.click(screen.getByRole('button', { name: /refresh streaming/i }))
+ await waitFor(() => expect(screen.getByText('Refreshed!')).toBeInTheDocument())
})
it('dispatches streaming-refreshed event on success', async () => {
@@ -45,7 +45,7 @@ describe('StreamingRefreshButton', () => {
const listener = vi.fn()
window.addEventListener('streaming-refreshed', listener)
render()
- fireEvent.click(screen.getByText('📡 Refresh Streaming'))
+ fireEvent.click(screen.getByRole('button', { name: /refresh streaming/i }))
await waitFor(() => expect(listener).toHaveBeenCalled())
window.removeEventListener('streaming-refreshed', listener)
})
@@ -53,7 +53,7 @@ describe('StreamingRefreshButton', () => {
it('shows error state when refresh fails', async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false } as any)
render()
- fireEvent.click(screen.getByText('📡 Refresh Streaming'))
- await waitFor(() => expect(screen.getByText('❌ Failed')).toBeInTheDocument())
+ fireEvent.click(screen.getByRole('button', { name: /refresh streaming/i }))
+ await waitFor(() => expect(screen.getByText('Failed')).toBeInTheDocument())
})
})
diff --git a/tests/sidebar.test.tsx b/tests/sidebar.test.tsx
new file mode 100644
index 0000000..6165594
--- /dev/null
+++ b/tests/sidebar.test.tsx
@@ -0,0 +1,39 @@
+// tests/sidebar.test.tsx
+import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+
+vi.mock('next/navigation', () => ({
+ usePathname: () => '/watchlist',
+}))
+
+vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({}),
+}))
+
+import { Sidebar } from '@/components/sidebar'
+
+describe('Sidebar', () => {
+ it('renders primary nav links', () => {
+ render()
+ expect(screen.getByRole('link', { name: /watch list/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /watched/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /add movie/i })).toBeInTheDocument()
+ expect(screen.getByRole('link', { name: /recommend/i })).toBeInTheDocument()
+ })
+
+ it('does not render Browse Criterion or Browse IMDB in the utility footer', () => {
+ render()
+ expect(screen.queryByText(/browse criterion/i)).not.toBeInTheDocument()
+ expect(screen.queryByText(/browse imdb/i)).not.toBeInTheDocument()
+ })
+
+ it('wraps nav emoji icons with aria-hidden', () => {
+ render()
+ const navLinks = screen.getAllByRole('link')
+ navLinks.forEach((link) => {
+ const iconSpan = link.querySelector('span[aria-hidden="true"]')
+ expect(iconSpan).not.toBeNull()
+ })
+ })
+})