build(ui): migrate from CRA/Jest to Vite/Vitest (#3311)

* feat: create vite project

* feat: it's alive!

* feat: `make dev` working!

* feat: replace custom serviceWorker with vite plugin

* test: replace Jest with Vitest

* fix: run prettier

* fix: skip eslint for now.

* chore: remove ui.old folder

* refactor: replace lodash.pick with simple destructuring

* fix: eslint errors (wip)

* fix: eslint errors (wip)

* fix: display-name eslint errors (wip)

* fix: no-console eslint errors (wip)

* fix: react-refresh/only-export-components eslint errors (wip)

* fix: react-refresh/only-export-components eslint errors (wip)

* fix: react-refresh/only-export-components eslint errors (wip)

* fix: react-refresh/only-export-components eslint errors (wip)

* fix: build

* fix: pwa manifest

* refactor: pwa manifest

* refactor: simplify PORT configuration

* refactor: rename simple JS files

* test: cover playlistUtils

* fix: react-image-lightbox

* feat(ui): add sourcemaps to help debug issues
This commit is contained in:
Deluan Quintão
2024-09-28 11:54:36 -04:00
committed by GitHub
parent dd48a23f92
commit fcdd30ba8f
212 changed files with 6231 additions and 31060 deletions
@@ -2,17 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-admin'
import { withWidth } from '@material-ui/core'
import { useAlbumsPerPage } from './index'
import config from '../config'
export const useGetHandleArtistClick = (width) => {
const [perPage] = useAlbumsPerPage(width)
return (id) => {
return config.devShowArtistPage && id !== config.variousArtistsId
? `/artist/${id}/show`
: `/album?filter={"artist_id":"${id}"}&order=ASC&sort=max_year&displayedFilters={"compilation":true}&perPage=${perPage}`
}
}
import { useGetHandleArtistClick } from './useGetHandleArtistClick'
export const ArtistLinkField = withWidth()(({
record,
@@ -8,6 +8,7 @@ export const DurationField = ({ source, ...rest }) => {
try {
return <span>{formatDuration(record[source])}</span>
} catch (e) {
// eslint-disable-next-line no-console
console.log('Error in DurationField! Record:', record)
return <span>00:00</span>
}
@@ -46,6 +46,8 @@ export const MultiLineTextField = memo(
},
)
MultiLineTextField.displayName = 'MultiLineTextField'
MultiLineTextField.defaultProps = {
addLabel: true,
firstLine: 0,
@@ -1,20 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import { useRecordContext } from 'react-admin'
export const formatRange = (record, source) => {
const nameCapitalized = source.charAt(0).toUpperCase() + source.slice(1)
const min = record[`min${nameCapitalized}`]
const max = record[`max${nameCapitalized}`]
let range = []
if (min) {
range.push(min)
}
if (max && max !== min) {
range.push(max)
}
return range.join('-')
}
import { formatRange } from './formatRange'
export const RangeField = ({ className, source, ...rest }) => {
const record = useRecordContext(rest)
@@ -100,6 +100,8 @@ const ReleaseRow = forwardRef(
},
)
ReleaseRow.displayName = 'ReleaseRow'
const DiscSubtitleRow = forwardRef(
({ record, onClick, colSpan, contextAlwaysVisible }, ref) => {
const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'))
@@ -146,6 +148,8 @@ const DiscSubtitleRow = forwardRef(
},
)
DiscSubtitleRow.displayName = 'DiscSubtitleRow'
export const SongDatagridRow = ({
record,
children,
+12
View File
@@ -0,0 +1,12 @@
import { Children, cloneElement, isValidElement } from 'react'
import { isWritable } from './playlistUtils.js'
export const Writable = (props) => {
const { record = {}, children } = props
if (isWritable(record.ownerId)) {
return Children.map(children, (child) =>
isValidElement(child) ? cloneElement(child, props) : child,
)
}
return null
}
+13
View File
@@ -0,0 +1,13 @@
export const formatRange = (record, source) => {
const nameCapitalized = source.charAt(0).toUpperCase() + source.slice(1)
const min = record[`min${nameCapitalized}`]
const max = record[`max${nameCapitalized}`]
let range = []
if (min) {
range.push(min)
}
if (max && max !== min) {
range.push(max)
}
return range.join('-')
}
+3
View File
@@ -25,6 +25,7 @@ export * from './LoveButton'
export * from './Title'
export * from './SongBulkActions'
export * from './useAlbumsPerPage'
export * from './useGetHandleArtistClick'
export * from './useInterval'
export * from './useResourceRefresh'
export * from './useToggleLove'
@@ -37,3 +38,5 @@ export * from './useRating'
export * from './useSelectedFields'
export * from './ToggleFieldsMenu'
export * from './QualityInfo'
export * from './formatRange.js'
export * from './playlistUtils.js'
@@ -1,5 +1,3 @@
import { cloneElement, Children, isValidElement } from 'react'
export const isWritable = (ownerId) => {
return (
localStorage.getItem('userId') === ownerId ||
@@ -11,16 +9,6 @@ export const isReadOnly = (ownerId) => {
return !isWritable(ownerId)
}
export const Writable = (props) => {
const { record = {}, children } = props
if (isWritable(record.ownerId)) {
return Children.map(children, (child) =>
isValidElement(child) ? cloneElement(child, props) : child,
)
}
return null
}
export const isSmartPlaylist = (pls) => !!pls.rules
export const canChangeTracks = (pls) =>
+78
View File
@@ -0,0 +1,78 @@
import {
isWritable,
isReadOnly,
isSmartPlaylist,
canChangeTracks,
} from './playlistUtils'
describe('playlistUtils', () => {
beforeEach(() => {
localStorage.clear()
})
describe('isWritable', () => {
it('returns true if user is the owner', () => {
localStorage.setItem('userId', 'user1')
expect(isWritable('user1')).toBe(true)
})
it('returns true if user is an admin', () => {
localStorage.setItem('role', 'admin')
expect(isWritable('user1')).toBe(true)
})
it('returns false if user is not the owner and not an admin', () => {
localStorage.setItem('userId', 'user2')
expect(isWritable('user1')).toBe(false)
})
})
describe('isReadOnly', () => {
it('returns true if user is not the owner and not an admin', () => {
localStorage.setItem('userId', 'user2')
expect(isReadOnly('user1')).toBe(true)
})
it('returns false if user is the owner', () => {
localStorage.setItem('userId', 'user1')
expect(isReadOnly('user1')).toBe(false)
})
it('returns false if user is an admin', () => {
localStorage.setItem('role', 'admin')
expect(isReadOnly('user1')).toBe(false)
})
})
describe('isSmartPlaylist', () => {
it('returns true if playlist has rules', () => {
const playlist = { rules: [] }
expect(isSmartPlaylist(playlist)).toBe(true)
})
it('returns false if playlist does not have rules', () => {
const playlist = {}
expect(isSmartPlaylist(playlist)).toBe(false)
})
})
describe('canChangeTracks', () => {
it('returns true if user is the owner and playlist is not smart', () => {
localStorage.setItem('userId', 'user1')
const playlist = { ownerId: 'user1' }
expect(canChangeTracks(playlist)).toBe(true)
})
it('returns false if user is not the owner', () => {
localStorage.setItem('userId', 'user2')
const playlist = { ownerId: 'user1' }
expect(canChangeTracks(playlist)).toBe(false)
})
it('returns false if playlist is smart', () => {
localStorage.setItem('userId', 'user1')
const playlist = { ownerId: 'user1', rules: [] }
expect(canChangeTracks(playlist)).toBe(false)
})
})
})
+11
View File
@@ -0,0 +1,11 @@
import { useAlbumsPerPage } from './useAlbumsPerPage'
import config from '../config.js'
export const useGetHandleArtistClick = (width) => {
const [perPage] = useAlbumsPerPage(width)
return (id) => {
return config.devShowArtistPage && id !== config.variousArtistsId
? `/artist/${id}/show`
: `/album?filter={"artist_id":"${id}"}&order=ASC&sort=max_year&displayedFilters={"compilation":true}&perPage=${perPage}`
}
}
@@ -25,6 +25,7 @@ export const useRating = (resource, record) => {
}
})
.catch((e) => {
// eslint-disable-next-line no-console
console.log('Error encountered: ' + e)
})
}, [dataProvider, record, resource])
@@ -35,6 +36,7 @@ export const useRating = (resource, record) => {
.setRating(id, val)
.then(refreshRating)
.catch((e) => {
// eslint-disable-next-line no-console
console.log('Error setting star rating: ', e)
notify('ra.page.error', 'warning')
if (mountedRef.current) {
+38 -28
View File
@@ -1,47 +1,57 @@
import { vi } from 'vitest'
import * as React from 'react'
import * as Redux from 'react-redux'
import * as RA from 'react-admin'
import { useResourceRefresh } from './useResourceRefresh'
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}))
vi.mock('react', async () => {
const actual = await vi.importActual('react')
return {
...actual,
useState: vi.fn(),
}
})
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}))
vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
return {
...actual,
useSelector: vi.fn(),
}
})
jest.mock('react-admin', () => ({
...jest.requireActual('react-admin'),
useRefresh: jest.fn(),
useDataProvider: jest.fn(),
}))
vi.mock('react-admin', async () => {
const actual = await vi.importActual('react-admin')
return {
...actual,
useRefresh: vi.fn(),
useDataProvider: vi.fn(),
}
})
describe('useResourceRefresh', () => {
const setState = jest.fn()
const setState = vi.fn()
const useStateMock = (initState) => [initState, setState]
const refresh = jest.fn()
const refresh = vi.fn()
const useRefreshMock = () => refresh
const getMany = jest.fn()
const getMany = vi.fn()
const useDataProviderMock = () => ({ getMany })
let lastTime
beforeEach(() => {
jest.spyOn(React, 'useState').mockImplementation(useStateMock)
jest.spyOn(RA, 'useRefresh').mockImplementation(useRefreshMock)
jest.spyOn(RA, 'useDataProvider').mockImplementation(useDataProviderMock)
vi.spyOn(React, 'useState').mockImplementation(useStateMock)
vi.spyOn(RA, 'useRefresh').mockImplementation(useRefreshMock)
vi.spyOn(RA, 'useDataProvider').mockImplementation(useDataProviderMock)
lastTime = new Date(new Date().valueOf() + 1000)
})
afterEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
it('stores last time checked, to avoid redundant runs', () => {
const useSelectorMock = () => ({ lastReceived: lastTime })
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh()
@@ -49,9 +59,9 @@ describe('useResourceRefresh', () => {
})
it("does not run again if lastTime didn't change", () => {
jest.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
vi.spyOn(React, 'useState').mockImplementation(() => [lastTime, setState])
const useSelectorMock = () => ({ lastReceived: lastTime })
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh()
@@ -64,7 +74,7 @@ describe('useResourceRefresh', () => {
lastReceived: lastTime,
resources: { '*': '*' },
})
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh()
@@ -77,7 +87,7 @@ describe('useResourceRefresh', () => {
lastReceived: lastTime,
resources: { album: ['*'] },
})
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh()
@@ -90,7 +100,7 @@ describe('useResourceRefresh', () => {
lastReceived: lastTime,
resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
})
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh()
@@ -107,7 +117,7 @@ describe('useResourceRefresh', () => {
lastReceived: lastTime,
resources: { '*': '*' },
})
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh('album')
@@ -120,7 +130,7 @@ describe('useResourceRefresh', () => {
lastReceived: lastTime,
resources: { album: ['al-1', 'al-2'], song: ['sg-1', 'sg-2'] },
})
jest.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
vi.spyOn(Redux, 'useSelector').mockImplementation(useSelectorMock)
useResourceRefresh('song')
@@ -31,6 +31,7 @@ export const useToggleLove = (resource, record = {}) => {
toggle(record.id)
.then(refreshRecord)
.catch((e) => {
// eslint-disable-next-line no-console
console.log('Error toggling love: ', e)
notify('ra.page.error', 'warning')
if (mountedRef.current) {
@@ -10,6 +10,7 @@ export function useTraceUpdate(props) {
return ps
}, {})
if (Object.keys(changedProps).length > 0) {
// eslint-disable-next-line no-console
console.log('Changed props:', changedProps)
}
prev.current = props