flnxi-Wu/components/__tests__/RankingPanel.test.tsx
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

353 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* RankingPanel 组件测试
* 测试排行榜面板的核心功能,包括位置恢复和居中状态
* @jest-environment jsdom
*/
import React from 'react'
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'
import '@testing-library/jest-dom'
// Mock fetch
const mockFetch = jest.fn()
global.fetch = mockFetch
// Mock localStorage
const localStorageMock = (() => {
let store: Record<string, string> = {}
return {
getItem: jest.fn((key: string) => store[key] || null),
setItem: jest.fn((key: string, value: string) => {
store[key] = value
}),
removeItem: jest.fn((key: string) => {
delete store[key]
}),
clear: jest.fn(() => {
store = {}
}),
get store() {
return store
},
}
})()
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
})
// Mock window dimensions
Object.defineProperty(window, 'innerWidth', { value: 1920, writable: true })
Object.defineProperty(window, 'innerHeight', { value: 1080, writable: true })
import RankingPanel from '../RankingPanel'
describe('RankingPanel 组件', () => {
beforeEach(() => {
jest.clearAllMocks()
localStorageMock.clear()
jest.useFakeTimers()
// Mock API response
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
contributors: [
{ username: 'user1', count: 10 },
{ username: 'user2', count: 8 },
],
downloads: [
{ username: 'user3', count: 100 },
],
likes: [
{ username: 'user4', count: 50 },
],
}),
})
})
afterEach(() => {
jest.useRealTimers()
})
describe('基础渲染测试', () => {
it('应该正确渲染排行榜面板', async () => {
await act(async () => {
render(<RankingPanel />)
})
// 使用 getAllByText 因为有多个元素包含相同文本
const elements = screen.getAllByText('贡献榜')
expect(elements.length).toBeGreaterThan(0)
})
it('应该显示排行榜数据', async () => {
await act(async () => {
render(<RankingPanel />)
})
await waitFor(() => {
expect(screen.getByText('user1')).toBeInTheDocument()
expect(screen.getByText('10')).toBeInTheDocument()
})
})
})
describe('位置恢复测试', () => {
it('应该从 localStorage 恢复按钮位置', async () => {
// 设置保存的位置(非居中)
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 100,
y: 200,
centered: false,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
// 验证 localStorage.getItem 被调用
expect(localStorageMock.getItem).toHaveBeenCalledWith('rankingPanelButtonPosition')
// 按钮应该显示(因为面板已关闭)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 验证位置样式
expect(button).toHaveStyle({ left: '100px', top: '200px' })
})
it('应该恢复居中状态为 true', async () => {
// 设置保存的位置(居中)
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 32,
y: 0,
centered: true,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 居中状态下,top 应该是 50%,transform 应该包含 translateY(-50%)
expect(button).toHaveStyle({ left: '32px', top: '50%' })
})
it('应该恢复居中状态为 false', async () => {
// 设置保存的位置(非居中)
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 50,
y: 300,
centered: false,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 非居中状态下,top 应该是具体的像素值
expect(button).toHaveStyle({ left: '50px', top: '300px' })
})
it('没有 centered 字段时应该默认为 false', async () => {
// 设置保存的位置(旧格式,没有 centered 字段)
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 80,
y: 150,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 没有 centered 字段时,应该使用具体的 y 值
expect(button).toHaveStyle({ left: '80px', top: '150px' })
})
})
describe('关闭和打开测试', () => {
it('关闭面板时应该保存居中状态', async () => {
await act(async () => {
render(<RankingPanel />)
})
// 使用 getAllByText 因为有多个元素包含相同文本
const elements = screen.getAllByText('贡献榜')
expect(elements.length).toBeGreaterThan(0)
// 点击关闭按钮
const closeButton = screen.getByLabelText('关闭')
await act(async () => {
fireEvent.click(closeButton)
})
// 验证保存了位置和居中状态
expect(localStorageMock.setItem).toHaveBeenCalledWith(
'rankingPanelButtonPosition',
expect.stringContaining('"centered"')
)
})
it('打开面板时应该恢复位置', async () => {
localStorageMock.setItem('rankingPanelClosed', 'true')
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 100,
y: 200,
centered: false,
}))
await act(async () => {
render(<RankingPanel />)
})
// 点击按钮打开面板
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 模拟快速点击(非拖动)- 需要在 act 中执行
await act(async () => {
fireEvent.mouseDown(button!, { clientX: 100, clientY: 200 })
fireEvent.click(button!, { clientX: 100, clientY: 200 })
})
// 验证 localStorage.removeItem 被调用
expect(localStorageMock.removeItem).toHaveBeenCalledWith('rankingPanelClosed')
})
})
describe('拖动测试', () => {
it('拖动按钮后应该更新位置', async () => {
localStorageMock.setItem('rankingPanelClosed', 'true')
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 32,
y: 0,
centered: true,
}))
await act(async () => {
render(<RankingPanel />)
})
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 模拟拖动开始
await act(async () => {
fireEvent.mouseDown(button!, { clientX: 32, clientY: 540 })
})
// 模拟鼠标移动
await act(async () => {
fireEvent.mouseMove(document, { clientX: 200, clientY: 300 })
})
// 模拟鼠标释放
await act(async () => {
fireEvent.mouseUp(document)
})
// 验证 setItem 被调用(位置被保存)
const savedCalls = localStorageMock.setItem.mock.calls.filter(
(call: any[]) => call[0] === 'rankingPanelButtonPosition'
)
// 应该有保存位置的调用
expect(savedCalls.length).toBeGreaterThan(0)
})
})
describe('边界情况测试', () => {
it('应该处理无效的 JSON 数据', async () => {
localStorageMock.setItem('rankingPanelButtonPosition', 'invalid json')
localStorageMock.setItem('rankingPanelClosed', 'true')
// 不应该抛出错误
expect(() => render(<RankingPanel />)).not.toThrow()
})
it('应该处理空的 localStorage', async () => {
render(<RankingPanel />)
await waitFor(() => {
expect(screen.getByText('贡献榜')).toBeInTheDocument()
})
})
it('应该将位置限制在视口内', async () => {
// 设置超出视口的位置
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 9999,
y: 9999,
centered: false,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toBeInTheDocument()
// 位置应该被限制在视口内
const style = button?.getAttribute('style')
expect(style).toBeDefined()
})
})
})
describe('RankingPanel 位置恢复逻辑', () => {
beforeEach(() => {
jest.clearAllMocks()
localStorageMock.clear()
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
contributors: [],
downloads: [],
likes: [],
}),
})
})
it('centered: true 时按钮应该垂直居中', async () => {
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 32,
y: 0,
centered: true,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toHaveStyle({
left: '32px',
top: '50%',
transform: 'translateY(-50%)',
})
})
it('centered: false 时按钮应该使用具体坐标', async () => {
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 100,
y: 200,
centered: false,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
render(<RankingPanel />)
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toHaveStyle({
left: '100px',
top: '200px',
transform: 'none',
})
})
})