- 新增许可证服务器 (license-server/) - 新增 Agent 部署模块 (flnxi-agent/) - 添加许可证验证和激活页面 - 添加 Harbor 镜像同步脚本 - 更新素材系统支持许可证验证
368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
/**
|
||
* 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',
|
||
})
|
||
})
|
||
})
|