flnxi-Wu/components/__tests__/RankingPanel.test.tsx
WuYongJie ba98bf22cc feat: 添加许可证系统、Agent部署模块和Harbor镜像管理
- 新增许可证服务器 (license-server/)
- 新增 Agent 部署模块 (flnxi-agent/)
- 添加许可证验证和激活页面
- 添加 Harbor 镜像同步脚本
- 更新素材系统支持许可证验证
2025-12-15 21:19:46 +08:00

368 lines
11 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')
// 不应该抛出错误
await act(async () => {
expect(() => render(<RankingPanel />)).not.toThrow()
})
})
it('应该处理空的 localStorage', async () => {
await act(async () => {
render(<RankingPanel />)
})
// 使用 getAllByText 因为有多个元素包含相同文本
const elements = screen.getAllByText('贡献榜')
expect(elements.length).toBeGreaterThan(0)
})
it('应该将位置限制在视口内', async () => {
// 设置超出视口的位置
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 9999,
y: 9999,
centered: false,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
await act(async () => {
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()
jest.useFakeTimers()
mockFetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({
contributors: [],
downloads: [],
likes: [],
}),
})
})
afterEach(() => {
jest.useRealTimers()
})
it('centered: true 时按钮应该垂直居中', async () => {
localStorageMock.setItem('rankingPanelButtonPosition', JSON.stringify({
x: 32,
y: 0,
centered: true,
}))
localStorageMock.setItem('rankingPanelClosed', 'true')
await act(async () => {
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')
await act(async () => {
render(<RankingPanel />)
})
const button = document.querySelector('button[title="显示排行榜(可拖动)"]')
expect(button).toHaveStyle({
left: '100px',
top: '200px',
transform: 'none',
})
})
})