+
{showBlurHash && (
-
+ )}
+ {imgUrl && (
+

)}
-
)
}
@@ -58,6 +69,7 @@ CoverImage.propTypes = {
record: PropTypes.object,
size: PropTypes.number,
square: PropTypes.bool,
+ fit: PropTypes.oneOf(['cover', 'contain']),
className: PropTypes.string,
title: PropTypes.string,
onClick: PropTypes.func,
diff --git a/ui/src/common/CoverImage.test.jsx b/ui/src/common/CoverImage.test.jsx
new file mode 100644
index 000000000..56459d5a5
--- /dev/null
+++ b/ui/src/common/CoverImage.test.jsx
@@ -0,0 +1,68 @@
+import { render } from '@testing-library/react'
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+vi.mock('./useImageUrl', () => ({ useImageUrl: vi.fn() }))
+vi.mock('../subsonic', () => ({
+ default: { getCoverArtUrl: () => '/rest/getCoverArt?id=al-1' },
+}))
+vi.mock('../config', () => ({ default: { uiCoverArtSize: 300 } }))
+
+import { useImageUrl } from './useImageUrl'
+import { CoverImage } from './CoverImage'
+
+const withArt = {
+ id: 'al-1',
+ name: 'Album',
+ blurHash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
+}
+
+describe('CoverImage', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ // jsdom has no 2D context; stub it so BlurHashCanvas bails cleanly without console noise
+ HTMLCanvasElement.prototype.getContext = vi.fn(() => null)
+ })
+
+ it('renders nothing without a record', () => {
+ useImageUrl.mockReturnValue({ imgUrl: null, loading: false })
+ const { container } = render(
)
+ expect(container.firstChild).toBeNull()
+ })
+
+ it('shows the blurhash and no
![]()
while loading', () => {
+ useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
+ const { container } = render(
)
+ expect(container.querySelector('canvas')).not.toBeNull()
+ expect(container.querySelector('img')).toBeNull()
+ })
+
+ it('shows neither a broken
![]()
nor a canvas while loading a record with no blurhash', () => {
+ useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
+ const { container } = render(
)
+ expect(container.querySelector('img')).toBeNull()
+ expect(container.querySelector('canvas')).toBeNull()
+ })
+
+ it('mounts the image only once its blob is ready', () => {
+ useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
+ const { container } = render(
)
+ const img = container.querySelector('img')
+ expect(img).not.toBeNull()
+ expect(img.getAttribute('src')).toBe('blob:abc')
+ })
+
+ it('fires onClick only when the image is loaded', () => {
+ const onClick = vi.fn()
+ useImageUrl.mockReturnValue({ imgUrl: null, loading: true })
+ const { container, rerender } = render(
+
,
+ )
+ container.firstChild.click()
+ expect(onClick).not.toHaveBeenCalled()
+
+ useImageUrl.mockReturnValue({ imgUrl: 'blob:abc', loading: false })
+ rerender(
)
+ container.firstChild.click()
+ expect(onClick).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx
index 692c0572d..800a7e354 100644
--- a/ui/src/playlist/PlaylistDetails.jsx
+++ b/ui/src/playlist/PlaylistDetails.jsx
@@ -1,10 +1,5 @@
import { useState } from 'react'
-import {
- Card,
- CardContent,
- Typography,
- useMediaQuery,
-} from '@material-ui/core'
+import { Card, CardContent, Typography, useMediaQuery } from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import { useTranslate } from 'react-admin'
import Lightbox from 'react-image-lightbox'
@@ -74,9 +69,6 @@ const useStyles = makeStyles(
backgroundColor: 'transparent',
transition: 'opacity 0.3s ease-in-out',
},
- coverLoading: {
- opacity: 0.5,
- },
title: {
overflow: 'hidden',
textOverflow: 'ellipsis',
@@ -117,6 +109,7 @@ const PlaylistDetails = (props) => {
setLightboxOpen(true)}
diff --git a/ui/src/radio/RadioList.jsx b/ui/src/radio/RadioList.jsx
index 2dd00feef..b127e84a8 100644
--- a/ui/src/radio/RadioList.jsx
+++ b/ui/src/radio/RadioList.jsx
@@ -22,7 +22,6 @@ import {
useSelectedFields,
} from '../common'
import { CoverImage } from '../common/CoverImage'
-import subsonic from '../subsonic'
import { StreamField } from './StreamField'
import { setTrack } from '../actions'
import { songFromRadio } from './helper'
@@ -103,7 +102,12 @@ const CoverArtField = ({ record }) => {
)
}
return (
-
+
)
}
CoverArtField.defaultProps = { label: '' }