feat: 增强情绪博物馆项目功能 - 新增用户评论和帖子功能,优化前端架构和WebSocket通信 - 更新文档和部署配置

This commit is contained in:
2025-07-29 07:38:47 +08:00
parent cc886cd4d5
commit 2f3d39fb00
142 changed files with 45645 additions and 0 deletions
+204
View File
@@ -0,0 +1,204 @@
/**
* E2E 测试自定义命令
*/
// 登录命令
Cypress.Commands.add('login', (username?: string, password?: string) => {
const user = username || Cypress.env('testUser').username
const pass = password || Cypress.env('testUser').password
cy.visit('/auth/login')
cy.get('[data-cy=username-input]').type(user)
cy.get('[data-cy=password-input]').type(pass)
cy.get('[data-cy=login-button]').click()
// 等待登录完成
cy.wait('@login')
cy.url().should('not.include', '/auth/login')
})
// 登出命令
Cypress.Commands.add('logout', () => {
cy.get('[data-cy=user-menu]').click()
cy.get('[data-cy=logout-button]').click()
cy.wait('@logout')
cy.url().should('include', '/auth/login')
})
// 等待页面加载完成
Cypress.Commands.add('waitForPageLoad', () => {
cy.get('[data-cy=loading]').should('not.exist')
cy.get('body').should('be.visible')
})
// 检查元素是否可见
Cypress.Commands.add('shouldBeVisible', (selector: string) => {
cy.get(selector).should('be.visible')
})
// 检查元素是否包含文本
Cypress.Commands.add('shouldContainText', (selector: string, text: string) => {
cy.get(selector).should('contain.text', text)
})
// 上传文件命令
Cypress.Commands.add('uploadFile', (selector: string, fileName: string) => {
cy.fixture(fileName, 'base64').then(fileContent => {
cy.get(selector).selectFile({
contents: Cypress.Buffer.from(fileContent, 'base64'),
fileName,
mimeType: 'image/jpeg'
}, { force: true })
})
})
// 等待 API 请求完成
Cypress.Commands.add('waitForApi', (alias: string) => {
cy.wait(`@${alias}`)
})
// 模拟网络延迟
Cypress.Commands.add('simulateNetworkDelay', (delay: number) => {
cy.intercept('**', (req) => {
req.reply((res) => {
return new Promise((resolve) => {
setTimeout(() => resolve(res), delay)
})
})
})
})
// 检查无障碍性
Cypress.Commands.add('checkA11y', () => {
cy.injectAxe()
cy.checkA11y()
})
// 自定义断言
Cypress.Commands.add('shouldHaveClass', { prevSubject: true }, (subject, className) => {
cy.wrap(subject).should('have.class', className)
})
Cypress.Commands.add('shouldNotHaveClass', { prevSubject: true }, (subject, className) => {
cy.wrap(subject).should('not.have.class', className)
})
// 表单填写命令
Cypress.Commands.add('fillForm', (formData: Record<string, string>) => {
Object.entries(formData).forEach(([field, value]) => {
cy.get(`[data-cy=${field}-input]`).clear().type(value)
})
})
// 等待元素出现
Cypress.Commands.add('waitForElement', (selector: string, timeout = 10000) => {
cy.get(selector, { timeout }).should('exist')
})
// 滚动到元素
Cypress.Commands.add('scrollToElement', (selector: string) => {
cy.get(selector).scrollIntoView()
})
// 模拟移动设备
Cypress.Commands.add('setMobileViewport', () => {
cy.viewport(375, 667) // iPhone 6/7/8 尺寸
})
// 模拟平板设备
Cypress.Commands.add('setTabletViewport', () => {
cy.viewport(768, 1024) // iPad 尺寸
})
// 模拟桌面设备
Cypress.Commands.add('setDesktopViewport', () => {
cy.viewport(1280, 720)
})
// 检查响应式设计
Cypress.Commands.add('checkResponsive', (selector: string) => {
// 桌面
cy.setDesktopViewport()
cy.get(selector).should('be.visible')
// 平板
cy.setTabletViewport()
cy.get(selector).should('be.visible')
// 移动
cy.setMobileViewport()
cy.get(selector).should('be.visible')
// 恢复桌面
cy.setDesktopViewport()
})
// 模拟键盘导航
Cypress.Commands.add('navigateWithKeyboard', (selector: string) => {
cy.get('body').tab()
cy.focused().should('have.attr', 'data-cy', selector)
})
// 检查加载状态
Cypress.Commands.add('shouldBeLoading', (selector: string) => {
cy.get(selector).should('have.class', 'loading')
})
Cypress.Commands.add('shouldNotBeLoading', (selector: string) => {
cy.get(selector).should('not.have.class', 'loading')
})
// 模拟网络错误
Cypress.Commands.add('simulateNetworkError', (url: string) => {
cy.intercept('GET', url, { forceNetworkError: true })
})
// 检查错误消息
Cypress.Commands.add('shouldShowError', (message: string) => {
cy.get('[data-cy=error-message]').should('contain.text', message)
})
// 检查成功消息
Cypress.Commands.add('shouldShowSuccess', (message: string) => {
cy.get('[data-cy=success-message]').should('contain.text', message)
})
// 清除通知
Cypress.Commands.add('clearNotifications', () => {
cy.get('[data-cy=notification-close]').each(($el) => {
cy.wrap($el).click()
})
})
// 等待动画完成
Cypress.Commands.add('waitForAnimation', (selector?: string) => {
if (selector) {
cy.get(selector).should('not.have.class', 'animating')
} else {
cy.wait(300) // 默认等待动画时间
}
})
// 模拟拖拽
Cypress.Commands.add('dragAndDrop', (sourceSelector: string, targetSelector: string) => {
cy.get(sourceSelector).trigger('mousedown', { which: 1 })
cy.get(targetSelector).trigger('mousemove').trigger('mouseup')
})
// 检查本地存储
Cypress.Commands.add('shouldHaveLocalStorage', (key: string, value?: string) => {
cy.window().its('localStorage').invoke('getItem', key).should('exist')
if (value) {
cy.window().its('localStorage').invoke('getItem', key).should('eq', value)
}
})
// 清除本地存储特定项
Cypress.Commands.add('clearLocalStorageItem', (key: string) => {
cy.window().its('localStorage').invoke('removeItem', key)
})
// 设置本地存储
Cypress.Commands.add('setLocalStorage', (key: string, value: string) => {
cy.window().its('localStorage').invoke('setItem', key, value)
})
+97
View File
@@ -0,0 +1,97 @@
/**
* E2E 测试支持文件
*/
// 导入 Cypress 命令
import './commands'
// 全局配置
Cypress.on('uncaught:exception', (err, runnable) => {
// 忽略某些预期的错误
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false
}
if (err.message.includes('Non-Error promise rejection captured')) {
return false
}
// 返回 false 阻止 Cypress 失败测试
return false
})
// 测试前钩子
beforeEach(() => {
// 清除本地存储
cy.clearLocalStorage()
cy.clearCookies()
// 设置视口
cy.viewport(1280, 720)
// 拦截 API 请求(可选)
cy.intercept('GET', '/api/user/profile', { fixture: 'user.json' }).as('getUserProfile')
cy.intercept('POST', '/api/auth/login', { fixture: 'auth.json' }).as('login')
cy.intercept('POST', '/api/auth/logout', { statusCode: 200 }).as('logout')
})
// 测试后钩子
afterEach(() => {
// 清理工作
cy.clearLocalStorage()
// 截图(失败时)
cy.screenshot({ capture: 'runner', onlyOnFailure: true })
})
// 自定义断言
declare global {
namespace Cypress {
interface Chainable {
/**
* 登录用户
*/
login(username?: string, password?: string): Chainable<void>
/**
* 登出用户
*/
logout(): Chainable<void>
/**
* 等待页面加载完成
*/
waitForPageLoad(): Chainable<void>
/**
* 检查元素是否可见
*/
shouldBeVisible(selector: string): Chainable<void>
/**
* 检查元素是否包含文本
*/
shouldContainText(selector: string, text: string): Chainable<void>
/**
* 上传文件
*/
uploadFile(selector: string, fileName: string): Chainable<void>
/**
* 等待 API 请求完成
*/
waitForApi(alias: string): Chainable<void>
/**
* 模拟网络延迟
*/
simulateNetworkDelay(delay: number): Chainable<void>
/**
* 检查无障碍性
*/
checkA11y(): Chainable<void>
}
}
}