feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,404 @@
import { test, expect } from '@playwright/test';
test.describe('黄历搜索功能E2E测试', () => {
let baseUrl: string;
test.beforeAll(async () => {
baseUrl = 'http://localhost:8081';
});
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/almanac-search/index`);
await page.waitForLoadState('networkidle');
});
test.describe('TC-SEARCH-001: 搜索条件添加流程', () => {
test('应该能够添加搜索条件', async ({ page }) => {
const addButton = page.locator('.add-condition-button, [class*="add-condition"], button:has-text("添加条件")');
if (await addButton.isVisible()) {
const initialCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
await addButton.click();
await page.waitForTimeout(500);
const newCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
expect(newCount).toBe(initialCount + 1);
} else {
test.skip();
}
});
test('应该能够添加多个搜索条件', async ({ page }) => {
const addButton = page.locator('.add-condition-button, [class*="add-condition"], button:has-text("添加条件")');
if (await addButton.isVisible()) {
for (let i = 0; i < 3; i++) {
await addButton.click();
await page.waitForTimeout(300);
}
const conditionCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
expect(conditionCount).toBeGreaterThanOrEqual(3);
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-002: 搜索条件删除流程', () => {
test('应该能够删除搜索条件', async ({ page }) => {
const addButton = page.locator('.add-condition-button, [class*="add-condition"], button:has-text("添加条件")');
if (await addButton.isVisible()) {
await addButton.click();
await page.waitForTimeout(500);
const deleteButton = page.locator('.delete-button, [class*="delete"], button:has-text("删除")').first();
if (await deleteButton.isVisible()) {
const initialCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
await deleteButton.click();
await page.waitForTimeout(300);
const newCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
expect(newCount).toBe(initialCount - 1);
}
} else {
test.skip();
}
});
test('删除最后一个条件后应该能够重新添加', async ({ page }) => {
const addButton = page.locator('.add-condition-button, [class*="add-condition"], button:has-text("添加条件")');
if (await addButton.isVisible()) {
await addButton.click();
await page.waitForTimeout(500);
const deleteButton = page.locator('.delete-button, [class*="delete"], button:has-text("删除")').first();
if (await deleteButton.isVisible()) {
await deleteButton.click();
await page.waitForTimeout(300);
await addButton.click();
await page.waitForTimeout(300);
const conditionCount = await page.locator('.search-condition-item, [class*="SearchConditionItem"]').count();
expect(conditionCount).toBeGreaterThan(0);
}
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-003: 搜索条件保存流程', () => {
test('应该能够保存搜索条件', async ({ page }) => {
const saveButton = page.locator('.save-condition-button, [class*="save-condition"], button:has-text("保存条件")');
if (await saveButton.isVisible()) {
await saveButton.click();
await page.waitForTimeout(500);
const modal = page.locator('.modal, [class*="modal"], .dialog').first();
if (await modal.isVisible()) {
const nameInput = page.locator('input[type="text"], [class*="input"]').first();
if (await nameInput.isVisible()) {
await nameInput.fill('测试搜索条件');
const confirmButton = page.locator('button:has-text("确认"), button:has-text("保存")').first();
if (await confirmButton.isVisible()) {
await confirmButton.click();
await page.waitForTimeout(500);
const savedConditions = page.locator('.saved-condition, [class*="saved-condition"]');
expect(await savedConditions.count()).toBeGreaterThan(0);
}
}
}
} else {
test.skip();
}
});
test('保存的条件应该显示在已保存条件列表中', async ({ page }) => {
const saveButton = page.locator('.save-condition-button, [class*="save-condition"], button:has-text("保存条件")');
if (await saveButton.isVisible()) {
await saveButton.click();
await page.waitForTimeout(500);
const modal = page.locator('.modal, [class*="modal"], .dialog').first();
if (await modal.isVisible()) {
const nameInput = page.locator('input[type="text"], [class*="input"]').first();
if (await nameInput.isVisible()) {
await nameInput.fill('测试条件');
const confirmButton = page.locator('button:has-text("确认"), button:has-text("保存")').first();
if (await confirmButton.isVisible()) {
await confirmButton.click();
await page.waitForTimeout(500);
const savedCondition = page.locator('.saved-condition:has-text("测试条件"), [class*="saved-condition"]:has-text("测试条件")');
expect(await savedCondition.isVisible()).toBe(true);
}
}
}
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-004: 搜索条件加载流程', () => {
test('应该能够加载已保存的搜索条件', async ({ page }) => {
const savedCondition = page.locator('.saved-condition, [class*="saved-condition"]').first();
if (await savedCondition.isVisible()) {
await savedCondition.click();
await page.waitForTimeout(500);
const conditionItems = page.locator('.search-condition-item, [class*="SearchConditionItem"]');
expect(await conditionItems.count()).toBeGreaterThan(0);
} else {
test.skip();
}
});
test('加载的条件应该正确显示', async ({ page }) => {
const savedCondition = page.locator('.saved-condition, [class*="saved-condition"]').first();
if (await savedCondition.isVisible()) {
await savedCondition.click();
await page.waitForTimeout(500);
const conditionItems = page.locator('.search-condition-item, [class*="SearchConditionItem"]');
if (await conditionItems.count() > 0) {
const firstCondition = conditionItems.first();
expect(await firstCondition.isVisible()).toBe(true);
}
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-005: 搜索执行流程', () => {
test('应该能够执行搜索', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(1000);
const loadingIndicator = page.locator('.loading-indicator, [class*="loading"]');
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
const loadingVisible = await loadingIndicator.isVisible().catch(() => false);
const resultsVisible = await results.isVisible().catch(() => false);
expect(loadingVisible || resultsVisible).toBe(true);
} else {
test.skip();
}
});
test('搜索过程中应该显示加载状态', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
const loadingIndicator = page.locator('.loading-indicator, [class*="loading"]');
expect(await loadingIndicator.isVisible()).toBe(true);
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-006: 结果排序流程', () => {
test('应该能够按日期排序', async ({ page }) => {
const sortOption = page.locator('.sort-option[data-sort="date"], [class*="sort-option"]:has-text("日期")');
if (await sortOption.isVisible()) {
await sortOption.click();
await page.waitForTimeout(500);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
if (await results.count() > 0) {
expect(await results.first().isVisible()).toBe(true);
}
} else {
test.skip();
}
});
test('应该能够按匹配度排序', async ({ page }) => {
const sortOption = page.locator('.sort-option[data-sort="matchCount"], [class*="sort-option"]:has-text("匹配度")');
if (await sortOption.isVisible()) {
await sortOption.click();
await page.waitForTimeout(500);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
if (await results.count() > 0) {
expect(await results.first().isVisible()).toBe(true);
}
} else {
test.skip();
}
});
test('应该能够切换升序/降序', async ({ page }) => {
const sortArrow = page.locator('.sort-arrow, [class*="sort-arrow"]');
if (await sortArrow.isVisible()) {
await sortArrow.click();
await page.waitForTimeout(500);
expect(await sortArrow.isVisible()).toBe(true);
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-007: 结果加载流程', () => {
test('应该能够加载搜索结果', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(2000);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
const resultCount = await results.count();
if (resultCount > 0) {
expect(await results.first().isVisible()).toBe(true);
}
} else {
test.skip();
}
});
test('应该支持无限滚动加载', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(2000);
const initialCount = await page.locator('.search-result-card, [class*="SearchResultCard"]').count();
if (initialCount > 0) {
await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
});
await page.waitForTimeout(1000);
const newCount = await page.locator('.search-result-card, [class*="SearchResultCard"]').count();
expect(newCount).toBeGreaterThanOrEqual(initialCount);
}
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-008: 空状态显示', () => {
test('无结果时应该显示空状态', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(2000);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
const resultCount = await results.count();
if (resultCount === 0) {
const emptyState = page.locator('.empty-state, [class*="EmptyState"]');
expect(await emptyState.isVisible()).toBe(true);
}
} else {
test.skip();
}
});
test('空状态应该显示提示信息', async ({ page }) => {
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(2000);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
const resultCount = await results.count();
if (resultCount === 0) {
const emptyState = page.locator('.empty-state, [class*="EmptyState"]');
if (await emptyState.isVisible()) {
const text = await emptyState.textContent();
expect(text).toBeTruthy();
expect(text!.length).toBeGreaterThan(0);
}
}
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-009: 错误状态处理', () => {
test('网络错误时应该显示错误提示', async ({ page }) => {
await page.route('**/api/**', route => route.abort());
const searchButton = page.locator('.search-button, [class*="search-button"], button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(2000);
const errorState = page.locator('.error-state, [class*="error"], .error-message');
const errorVisible = await errorState.isVisible().catch(() => false);
expect(errorVisible).toBe(true);
} else {
test.skip();
}
});
});
test.describe('TC-SEARCH-010: 响应式设计', () => {
test('应该在移动端正常显示', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.reload();
await page.waitForLoadState('networkidle');
const searchPanel = page.locator('.search-condition-panel, [class*="SearchConditionPanel"]');
expect(await searchPanel.isVisible()).toBe(true);
});
test('应该在桌面端正常显示', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.reload();
await page.waitForLoadState('networkidle');
const searchPanel = page.locator('.search-condition-panel, [class*="SearchConditionPanel"]');
expect(await searchPanel.isVisible()).toBe(true);
});
test('应该在平板端正常显示', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.reload();
await page.waitForLoadState('networkidle');
const searchPanel = page.locator('.search-condition-panel, [class*="SearchConditionPanel"]');
expect(await searchPanel.isVisible()).toBe(true);
});
});
});
@@ -0,0 +1,258 @@
import { test, expect } from '@playwright/test';
test.describe('关键词搜索功能E2E测试', () => {
let baseUrl: string;
test.beforeAll(async () => {
baseUrl = 'http://localhost:8081';
});
test.describe('TC-KEYWORD-001: 首页搜索入口', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
});
test('应该显示搜索入口', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
expect(await searchEntry.isVisible()).toBe(true);
});
test('点击搜索入口应该打开搜索面板', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const searchPanel = page.locator('.search-condition-panel, [class*="SearchConditionPanel"]');
expect(await searchPanel.isVisible()).toBe(true);
});
test('搜索面板应该包含关键词输入框', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"], input[placeholder*="搜索"]');
expect(await keywordInput.isVisible()).toBe(true);
});
});
test.describe('TC-KEYWORD-002: 关键词搜索流程', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
});
test('应该能够输入关键词进行搜索', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"], input[placeholder*="搜索"]');
await keywordInput.fill('祭祀');
await page.waitForTimeout(300);
const searchButton = page.locator('.search-btn, button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(1000);
await page.waitForURL(/almanac-search/, { timeout: 5000 });
}
});
test('空关键词不应该触发搜索', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"], input[placeholder*="搜索"]');
await keywordInput.fill('');
await page.waitForTimeout(300);
const searchButton = page.locator('.search-btn, button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(500);
const currentUrl = page.url();
expect(currentUrl).not.toContain('keyword=');
}
});
test('应该能够清除关键词', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"], input[placeholder*="搜索"]');
await keywordInput.fill('测试关键词');
await page.waitForTimeout(300);
const clearButton = page.locator('.clear-btn, [class*="clear"]');
if (await clearButton.isVisible()) {
await clearButton.click();
await page.waitForTimeout(300);
const inputValue = await keywordInput.inputValue();
expect(inputValue).toBe('');
}
});
});
test.describe('TC-KEYWORD-003: 搜索历史', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
});
test('应该保存搜索历史', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"], input[placeholder*="搜索"]');
await keywordInput.fill('历史测试');
const searchButton = page.locator('.search-btn, button:has-text("搜索")');
if (await searchButton.isVisible()) {
await searchButton.click();
await page.waitForTimeout(1000);
}
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
await searchEntry.click();
await page.waitForTimeout(500);
const historyItem = page.locator('.history-item:has-text("历史测试"), [class*="history"]:has-text("历史测试")');
const historyVisible = await historyItem.isVisible().catch(() => false);
if (historyVisible) {
expect(historyVisible).toBe(true);
}
});
test('应该能够清除搜索历史', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const clearHistoryBtn = page.locator('.clear-history, button:has-text("清除历史")');
if (await clearHistoryBtn.isVisible()) {
await clearHistoryBtn.click();
await page.waitForTimeout(500);
const historyItems = page.locator('.history-item, [class*="history-item"]');
const count = await historyItems.count();
expect(count).toBe(0);
}
});
});
test.describe('TC-KEYWORD-004: 搜索结果页', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/almanac-search/index?keyword=%E7%A5%AD%E7%A5%80`);
await page.waitForLoadState('networkidle');
});
test('应该显示搜索结果', async ({ page }) => {
await page.waitForTimeout(2000);
const results = page.locator('.search-result-card, [class*="SearchResultCard"], .result-item');
const count = await results.count();
if (count > 0) {
expect(await results.first().isVisible()).toBe(true);
}
});
test('应该显示加载状态', async ({ page }) => {
const loadingIndicator = page.locator('.loading-indicator, [class*="loading"]');
const loadingVisible = await loadingIndicator.isVisible().catch(() => false);
if (loadingVisible) {
expect(loadingVisible).toBe(true);
}
});
test('无结果时应该显示空状态', async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/almanac-search/index?keyword=xyz123notexist`);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(2000);
const results = page.locator('.search-result-card, [class*="SearchResultCard"]');
const count = await results.count();
if (count === 0) {
const emptyState = page.locator('.empty-state, [class*="EmptyState"], .no-result');
const emptyVisible = await emptyState.isVisible().catch(() => false);
expect(emptyVisible).toBe(true);
}
});
});
test.describe('TC-KEYWORD-005: 高级搜索与关键词搜索切换', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
});
test('应该能够切换到高级搜索', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const advancedTab = page.locator('.tab:has-text("高级"), [class*="tab"]:has-text("高级")');
if (await advancedTab.isVisible()) {
await advancedTab.click();
await page.waitForTimeout(300);
const conditionPanel = page.locator('.condition-list, [class*="condition"]');
expect(await conditionPanel.isVisible()).toBe(true);
}
});
test('应该能够切换回关键词搜索', async ({ page }) => {
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
await searchEntry.click();
await page.waitForTimeout(500);
const advancedTab = page.locator('.tab:has-text("高级"), [class*="tab"]:has-text("高级")');
if (await advancedTab.isVisible()) {
await advancedTab.click();
await page.waitForTimeout(300);
const keywordTab = page.locator('.tab:has-text("关键词"), [class*="tab"]:has-text("关键词")');
if (await keywordTab.isVisible()) {
await keywordTab.click();
await page.waitForTimeout(300);
const keywordInput = page.locator('.keyword-input, input[placeholder*="关键词"]');
expect(await keywordInput.isVisible()).toBe(true);
}
}
});
});
test.describe('TC-KEYWORD-006: 响应式设计', () => {
test('移动端应该正常显示搜索入口', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
expect(await searchEntry.isVisible()).toBe(true);
});
test('桌面端应该正常显示搜索入口', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(`${baseUrl}/#/pages/index/index`);
await page.waitForLoadState('networkidle');
const searchEntry = page.locator('.search-entry, [class*="search-entry"], .search-box');
expect(await searchEntry.isVisible()).toBe(true);
});
});
});
@@ -0,0 +1,150 @@
import { test, expect } from '@playwright/test';
import { MiniProgramAlmanacPage } from '../pages/AlmanacPage';
test.describe('小程序黄历测试', () => {
let almanacPage: MiniProgramAlmanacPage;
test.beforeEach(async ({ page }) => {
almanacPage = new MiniProgramAlmanacPage(page);
await almanacPage.navigate();
});
test('应该显示当前日期', async () => {
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
test('应该显示农历日期', async () => {
const lunarDate = await almanacPage.getLunarDate();
expect(lunarDate).toBeTruthy();
});
test('应该显示干支', async () => {
const ganzhi = await almanacPage.getGanZhi();
expect(ganzhi).toBeTruthy();
});
test('应该显示生肖', async () => {
const zodiac = await almanacPage.getZodiac();
expect(zodiac).toBeTruthy();
});
test('应该显示节气', async () => {
const solarTerm = await almanacPage.getSolarTerm();
expect(solarTerm).toBeTruthy();
});
test('应该显示宜', async () => {
const suitable = await almanacPage.getSuitable();
expect(suitable).toBeTruthy();
});
test('应该显示忌', async () => {
const unsuitable = await almanacPage.getUnsuitable();
expect(unsuitable).toBeTruthy();
});
test('应该显示日期信息', async () => {
const dayInfo = await almanacPage.getDayInfo();
expect(dayInfo).toBeTruthy();
});
test('应该允许日期搜索', async () => {
const date = '2026-02-11';
await almanacPage.searchDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
test('应该能够导航到下一天', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToNextDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
test('应该能够导航到上一天', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToPreviousDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
test('应该显示今天指示器', async () => {
const isToday = await almanacPage.isToday();
expect(isToday).toBe(true);
});
test('应该显示黄历详情', async () => {
const details = await almanacPage.getAlmanacDetails();
expect(details).toBeTruthy();
});
test('应该允许点击日期', async () => {
const date = '2026-02-11';
await almanacPage.tapDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
test('应该允许长按日期', async ({ page }) => {
const date = '2026-02-11';
await almanacPage.longPressDate(date);
await page.waitForTimeout(1000);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
test('应该允许左滑到下一天', async ({ page }) => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeLeft();
await page.waitForTimeout(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
test('应该允许右滑到上一天', async ({ page }) => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeRight();
await page.waitForTimeout(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
test('应该允许分享黄历', async () => {
await almanacPage.shareAlmanac();
await page.waitForTimeout(1000);
const shareDialog = await page.$('.share-dialog');
const isVisible = await shareDialog?.isVisible();
expect(isVisible).toBe(true);
});
test('应该允许收藏日期', async () => {
await almanacPage.bookmarkDate();
await page.waitForTimeout(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(true);
});
test('应该允许取消收藏日期', async () => {
await almanacPage.bookmarkDate();
await page.waitForTimeout(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(false);
});
test('应该显示农历日历', async () => {
const lunarCalendar = await almanacPage.getLunarCalendar();
expect(lunarCalendar).toBeTruthy();
});
test('应该显示节日', async () => {
const festivals = await almanacPage.getFestivals();
expect(Array.isArray(festivals)).toBe(true);
});
test('应该显示有节日的节日指示器', async () => {
const hasFestival = await almanacPage.hasFestival();
expect(typeof hasFestival).toBe('boolean');
});
});
@@ -0,0 +1,114 @@
import { test, expect } from '@playwright/test';
import { MiniProgramCalendarPage } from '../pages/CalendarPage';
test.describe('小程序日历测试', () => {
let calendarPage: MiniProgramCalendarPage;
test.beforeEach(async ({ page }) => {
calendarPage = new MiniProgramCalendarPage(page);
await calendarPage.navigate();
});
test('应该显示当前日期', async () => {
const currentDate = await calendarPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
test('应该允许选择日期', async () => {
await calendarPage.selectDate('2026-02-11');
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain('2026-02-11');
});
test('应该能够导航到下个月', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToNextMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
test('应该能够导航到上个月', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToPreviousMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
test('应该显示今天指示器', async () => {
const today = new Date();
const todayDate = today.toISOString().split('T')[0];
const isToday = await calendarPage.isToday(todayDate);
expect(isToday).toBe(true);
});
test('应该显示周末指示器', async () => {
const weekendDate = '2026-02-15';
const isWeekend = await calendarPage.isWeekend(weekendDate);
expect(isWeekend).toBe(true);
});
test('应该允许点击日期', async () => {
const date = '2026-02-11';
await calendarPage.tapDate(date);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
test('应该允许长按日期', async ({ page }) => {
const date = '2026-02-11';
await calendarPage.longPressDate(date);
await page.waitForTimeout(1000);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
test('应该允许左滑到下个月', async ({ page }) => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeLeft();
await page.waitForTimeout(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
test('应该允许右滑到上个月', async ({ page }) => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeRight();
await page.waitForTimeout(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
test('应该显示日历事件', async () => {
const events = await calendarPage.getCalendarEvents();
expect(Array.isArray(events)).toBe(true);
});
test('应该显示有事件日期的事件指示器', async () => {
const dateWithEvent = '2026-02-11';
const hasEvent = await calendarPage.hasEvent(dateWithEvent);
expect(hasEvent).toBe(true);
});
test('应该显示当前月份的所有日期', async () => {
const date = '2026-02-15';
const isVisible = await calendarPage.isDateVisible(date);
expect(isVisible).toBe(true);
});
test('应该显示月份视图', async () => {
const monthView = await calendarPage.getMonthView();
expect(monthView).toBeTruthy();
});
test('应该显示星期几', async () => {
const weekDays = await calendarPage.getWeekDays();
expect(Array.isArray(weekDays)).toBe(true);
expect(weekDays.length).toBe(7);
});
test('应该显示月份中的所有日期', async () => {
const dates = await calendarPage.getDatesInMonth();
expect(Array.isArray(dates)).toBe(true);
expect(dates.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,122 @@
import { Page } from 'playwright';
export class MiniProgramAlmanacPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:9527/#/pages/almanac/index');
await this.page.waitForLoadState('networkidle');
}
async getCurrentDate() {
const currentDate = await this.page.locator('.current-date').textContent();
return currentDate || '';
}
async getLunarDate() {
const lunarDate = await this.page.locator('.lunar-date').textContent();
return lunarDate || '';
}
async getGanZhi() {
const ganzhi = await this.page.locator('.ganzhi').textContent();
return ganzhi || '';
}
async getZodiac() {
const zodiac = await this.page.locator('.zodiac').textContent();
return zodiac || '';
}
async getSolarTerm() {
const solarTerm = await this.page.locator('.solar-term').textContent();
return solarTerm || '';
}
async getSuitable() {
const suitable = await this.page.locator('.suitable').textContent();
return suitable || '';
}
async getUnsuitable() {
const unsuitable = await this.page.locator('.unsuitable').textContent();
return unsuitable || '';
}
async getDayInfo() {
const dayInfo = await this.page.locator('.day-info').textContent();
return dayInfo || '';
}
async searchDate(date: string) {
await this.page.fill('.date-search input', date);
await this.page.click('.date-search button');
await this.page.waitForLoadState('networkidle');
}
async navigateToNextDay() {
await this.page.click('.next-day');
}
async navigateToPreviousDay() {
await this.page.click('.previous-day');
}
async isToday() {
const todayIndicator = this.page.locator('.today-indicator');
return await todayIndicator.isVisible();
}
async getAlmanacDetails() {
const details = await this.page.locator('.almanac-details').textContent();
return details || '';
}
async tapDate(date: string) {
await this.page.tap(`[data-date="${date}"]`);
}
async longPressDate(date: string) {
const element = this.page.locator(`[data-date="${date}"]`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
async shareAlmanac() {
await this.page.click('.share-button');
}
async bookmarkDate() {
await this.page.click('.bookmark-button');
}
async isBookmarked() {
const bookmarked = this.page.locator('.bookmarked');
return await bookmarked.isVisible();
}
async getLunarCalendar() {
const lunarCalendar = await this.page.locator('.lunar-calendar').textContent();
return lunarCalendar || '';
}
async getFestivals() {
const festivals = await this.page.locator('.festivals').allTextContents();
return festivals;
}
async hasFestival() {
const festivalIndicator = this.page.locator('.festival-indicator');
return await festivalIndicator.isVisible();
}
}
@@ -0,0 +1,97 @@
import { Page } from 'playwright';
export class MiniProgramCalendarPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:9527/#/pages/calendar/index');
await this.page.waitForLoadState('networkidle');
}
async getCurrentDate() {
const currentDate = await this.page.locator('.current-date').textContent();
return currentDate || '';
}
async selectDate(date: string) {
await this.page.click(`[data-date="${date}"]`);
}
async getSelectedDate() {
const selectedDate = await this.page.locator('.selected-date').textContent();
return selectedDate || '';
}
async navigateToNextMonth() {
await this.page.click('.next-month');
}
async navigateToPreviousMonth() {
await this.page.click('.previous-month');
}
async getCurrentMonth() {
const currentMonth = await this.page.locator('.current-month').textContent();
return currentMonth || '';
}
async isDateVisible(date: string) {
const dateElement = this.page.locator(`[data-date="${date}"]`);
return await dateElement.isVisible();
}
async isToday(date: string) {
const todayElement = this.page.locator(`[data-date="${date}"].today`);
return await todayElement.isVisible();
}
async isWeekend(date: string) {
const weekendElement = this.page.locator(`[data-date="${date}"].weekend`);
return await weekendElement.isVisible();
}
async getCalendarEvents() {
const events = await this.page.locator('.calendar-event').allTextContents();
return events;
}
async hasEvent(date: string) {
const eventIndicator = this.page.locator(`[data-date="${date}"] .event-indicator`);
return await eventIndicator.isVisible();
}
async tapDate(date: string) {
await this.page.tap(`[data-date="${date}"]`);
}
async longPressDate(date: string) {
const element = this.page.locator(`[data-date="${date}"]`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
async getMonthView() {
const monthView = await this.page.locator('.calendar-month').textContent();
return monthView || '';
}
async getWeekDays() {
const weekDays = await this.page.locator('.week-day').allTextContents();
return weekDays;
}
async getDatesInMonth() {
const dates = await this.page.locator('.calendar-date').allTextContents();
return dates;
}
}
@@ -0,0 +1,178 @@
import { Page } from 'playwright';
export class MiniProgramSearchPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:9527/#/pages/search/index');
await this.page.waitForLoadState('networkidle');
}
async search(keyword: string) {
await this.page.fill('.search-input input', keyword);
await this.page.click('.search-button');
await this.page.waitForLoadState('networkidle');
}
async clearSearch() {
await this.page.click('.clear-button');
await this.page.waitForLoadState('networkidle');
}
async getSearchResults() {
const results = await this.page.locator('.search-result').allTextContents();
return results;
}
async getResultCount() {
const count = await this.page.locator('.search-result').count();
return count;
}
async hasResults() {
const count = await this.getResultCount();
return count > 0;
}
async noResults() {
const noResults = this.page.locator('.no-results');
return await noResults.isVisible();
}
async tapResult(index: number) {
await this.page.tap(`.search-result:nth-child(${index + 1})`);
}
async longPressResult(index: number) {
const element = this.page.locator(`.search-result:nth-child(${index + 1})`);
await element.tap();
await this.page.waitForTimeout(500);
}
async getResultTitle(index: number) {
const title = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-title`).textContent();
return title || '';
}
async getResultDescription(index: number) {
const description = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-description`).textContent();
return description || '';
}
async getResultDate(index: number) {
const date = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-date`).textContent();
return date || '';
}
async filterByType(type: string) {
await this.page.click(`.filter-type[data-type="${type}"]`);
await this.page.waitForLoadState('networkidle');
}
async filterByDate(startDate: string, endDate: string) {
await this.page.click('.filter-date');
await this.page.fill('.filter-start-date input', startDate);
await this.page.fill('.filter-end-date input', endDate);
await this.page.click('.apply-filter');
await this.page.waitForLoadState('networkidle');
}
async sortBy(sortType: string) {
await this.page.click('.sort-button');
await this.page.click(`.sort-option[data-sort="${sortType}"]`);
await this.page.waitForLoadState('networkidle');
}
async getCurrentSort() {
const currentSort = await this.page.locator('.current-sort').textContent();
return currentSort || '';
}
async getActiveFilters() {
const activeFilters = await this.page.locator('.active-filter').allTextContents();
return activeFilters;
}
async clearFilters() {
await this.page.click('.clear-filters');
await this.page.waitForLoadState('networkidle');
}
async saveSearch(keyword: string) {
await this.search(keyword);
await this.page.click('.save-search-button');
await this.page.waitForLoadState('networkidle');
}
async getSavedSearches() {
const savedSearches = await this.page.locator('.saved-search').allTextContents();
return savedSearches;
}
async deleteSavedSearch(keyword: string) {
await this.page.click(`.saved-search[data-keyword="${keyword}"] .delete-button`);
await this.page.waitForLoadState('networkidle');
}
async getRecentSearches() {
const recentSearches = await this.page.locator('.recent-search').allTextContents();
return recentSearches;
}
async clearRecentSearches() {
await this.page.click('.clear-recent');
await this.page.waitForLoadState('networkidle');
}
async getSearchSuggestions(keyword: string) {
await this.page.fill('.search-input input', keyword);
await this.page.waitForTimeout(500);
const suggestions = await this.page.locator('.search-suggestion').allTextContents();
return suggestions;
}
async selectSuggestion(index: number) {
await this.page.tap(`.search-suggestion:nth-child(${index + 1})`);
await this.page.waitForLoadState('networkidle');
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
async swipeUp() {
await this.page.touchscreen.tap(0, 100);
await this.page.touchscreen.tap(0, 0);
}
async swipeDown() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(0, 100);
}
async getSearchHistory() {
const searchHistory = await this.page.locator('.search-history').allTextContents();
return searchHistory;
}
async clearSearchHistory() {
await this.page.click('.clear-search-history');
await this.page.waitForLoadState('networkidle');
}
async getHotSearches() {
const hotSearches = await this.page.locator('.hot-search').allTextContents();
return hotSearches;
}
async tapHotSearch(index: number) {
await this.page.tap(`.hot-search:nth-child(${index + 1})`);
await this.page.waitForLoadState('networkidle');
}
}
@@ -0,0 +1,162 @@
import { Page } from 'playwright';
export class MiniProgramUserPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:9527/#/pages/user/index');
await this.page.waitForLoadState('networkidle');
}
async getUsername() {
const username = await this.page.locator('.username').textContent();
return username || '';
}
async getUserAvatar() {
const avatar = await this.page.locator('.user-avatar');
return await avatar.getAttribute('src');
}
async isLoggedIn() {
const loginButton = this.page.locator('.login-button');
return !(await loginButton.isVisible());
}
async login(username: string, password: string) {
await this.page.fill('.login-username input', username);
await this.page.fill('.login-password input', password);
await this.page.click('.login-button');
await this.page.waitForLoadState('networkidle');
}
async logout() {
await this.page.click('.logout-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToProfile() {
await this.page.click('.profile-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToSettings() {
await this.page.click('.settings-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToHistory() {
await this.page.click('.history-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToFavorites() {
await this.page.click('.favorites-button');
await this.page.waitForLoadState('networkidle');
}
async updateProfile(data: { username?: string; email?: string; phone?: string }) {
await this.navigateToProfile();
if (data.username) {
await this.page.fill('.profile-username input', data.username);
}
if (data.email) {
await this.page.fill('.profile-email input', data.email);
}
if (data.phone) {
await this.page.fill('.profile-phone input', data.phone);
}
await this.page.click('.save-button');
await this.page.waitForLoadState('networkidle');
}
async getProfile() {
const profile = {
username: await this.page.locator('.profile-username').textContent() || '',
email: await this.page.locator('.profile-email').textContent() || '',
phone: await this.page.locator('.profile-phone').textContent() || '',
};
return profile;
}
async toggleTheme() {
await this.page.click('.theme-toggle');
await this.page.waitForTimeout(500);
}
async getCurrentTheme() {
const theme = await this.page.locator('.current-theme').textContent();
return theme || '';
}
async getSettings() {
const settings = {
theme: await this.page.locator('.setting-theme').textContent() || '',
language: await this.page.locator('.setting-language').textContent() || '',
notifications: await this.page.locator('.setting-notifications').textContent() || '',
};
return settings;
}
async updateSetting(key: string, value: string) {
await this.navigateToSettings();
await this.page.click(`.setting-${key}`);
await this.page.click(`[data-value="${value}"]`);
await this.page.click('.save-button');
await this.page.waitForLoadState('networkidle');
}
async getHistory() {
const historyItems = await this.page.locator('.history-item').allTextContents();
return historyItems;
}
async clearHistory() {
await this.navigateToHistory();
await this.page.click('.clear-history-button');
await this.page.waitForLoadState('networkidle');
}
async getFavorites() {
const favorites = await this.page.locator('.favorite-item').allTextContents();
return favorites;
}
async removeFavorite(id: string) {
await this.navigateToFavorites();
await this.page.click(`[data-favorite-id="${id}"] .remove-button`);
await this.page.waitForLoadState('networkidle');
}
async tapButton(buttonClass: string) {
await this.page.tap(`.${buttonClass}`);
}
async longPressButton(buttonClass: string) {
const element = this.page.locator(`.${buttonClass}`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeUp() {
await this.page.touchscreen.tap(0, 100);
await this.page.touchscreen.tap(0, 0);
}
async swipeDown() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(0, 100);
}
async getUserInfo() {
const userInfo = await this.page.locator('.user-info').textContent();
return userInfo || '';
}
async getNavigationItems() {
const navItems = await this.page.locator('.nav-item').allTextContents();
return navItems;
}
}
@@ -0,0 +1,4 @@
export { MiniProgramCalendarPage } from './CalendarPage';
export { MiniProgramAlmanacPage } from './AlmanacPage';
export { MiniProgramUserPage } from './UserPage';
export { MiniProgramSearchPage } from './SearchPage';
@@ -0,0 +1,35 @@
const { spawn } = require('child_process');
const path = require('path');
const PLAYWRIGHT_CONFIG = path.join(__dirname, '../../playwright.miniprogram.config.ts');
function runTests() {
console.log('Starting mini program tests...');
console.log(`Using Playwright config: ${PLAYWRIGHT_CONFIG}`);
const playwright = spawn('npx', ['playwright', 'test', '--config', PLAYWRIGHT_CONFIG], {
stdio: 'inherit',
shell: true,
});
playwright.on('close', (code) => {
if (code === 0) {
console.log('Mini program tests passed!');
process.exit(0);
} else {
console.error(`Mini program tests failed with code ${code}`);
process.exit(code);
}
});
playwright.on('error', (error) => {
console.error('Failed to start Playwright:', error);
process.exit(1);
});
}
if (require.main === module) {
runTests();
}
module.exports = { runTests };
@@ -0,0 +1,234 @@
import { test, expect } from '@playwright/test';
import { MiniProgramSearchPage } from '../pages/SearchPage';
test.describe('小程序搜索测试', () => {
let searchPage: MiniProgramSearchPage;
test.beforeEach(async ({ page }) => {
searchPage = new MiniProgramSearchPage(page);
await searchPage.navigate();
});
test('应该显示搜索输入框', async () => {
const searchInput = await page.$('.search-input input');
const isVisible = await searchInput?.isVisible();
expect(isVisible).toBe(true);
});
test('应该允许搜索', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
test('应该允许清除搜索', async () => {
await searchPage.search('test');
await searchPage.clearSearch();
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(false);
});
test('应该显示搜索结果', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBeGreaterThan(0);
});
test('应该显示结果数量', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const count = await searchPage.getResultCount();
expect(count).toBeGreaterThan(0);
});
test('应该显示无结果提示', async () => {
const keyword = 'xyz123';
await searchPage.search(keyword);
const noResults = await searchPage.noResults();
expect(noResults).toBe(true);
});
test('应该允许点击结果', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.tapResult(0);
await page.waitForTimeout(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
test('应该允许长按结果', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.longPressResult(0);
await page.waitForTimeout(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
test('应该显示结果标题', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const title = await searchPage.getResultTitle(0);
expect(title).toBeTruthy();
});
test('应该显示结果描述', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const description = await searchPage.getResultDescription(0);
expect(description).toBeTruthy();
});
test('应该显示结果日期', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const date = await searchPage.getResultDate(0);
expect(date).toBeTruthy();
});
test('应该允许按类型筛选', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
test('应该允许按日期范围筛选', async () => {
const startDate = '2026-01-01';
const endDate = '2026-12-31';
await searchPage.filterByDate(startDate, endDate);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
test('应该允许排序', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.sortBy('date');
const currentSort = await searchPage.getCurrentSort();
expect(currentSort).toContain('date');
});
test('应该显示活动筛选器', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const activeFilters = await searchPage.getActiveFilters();
expect(Array.isArray(activeFilters)).toBe(true);
});
test('应该允许清除筛选器', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
await searchPage.clearFilters();
const activeFilters = await searchPage.getActiveFilters();
expect(activeFilters.length).toBe(0);
});
test('应该允许保存搜索', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).toContain(keyword);
});
test('应该显示保存的搜索', async () => {
const savedSearches = await searchPage.getSavedSearches();
expect(Array.isArray(savedSearches)).toBe(true);
});
test('应该允许删除保存的搜索', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
await searchPage.deleteSavedSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).not.toContain(keyword);
});
test('应该显示最近搜索', async () => {
const keyword = 'test';
await searchPage.search(keyword);
const recentSearches = await searchPage.getRecentSearches();
expect(Array.isArray(recentSearches)).toBe(true);
});
test('应该允许清除最近搜索', async () => {
await searchPage.clearRecentSearches();
const recentSearches = await searchPage.getRecentSearches();
expect(recentSearches.length).toBe(0);
});
test('应该显示搜索建议', async () => {
const keyword = '春';
const suggestions = await searchPage.getSearchSuggestions(keyword);
expect(Array.isArray(suggestions)).toBe(true);
});
test('应该允许选择建议', async () => {
const keyword = '春';
await searchPage.selectSuggestion(0);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
test('应该允许左滑', async ({ page }) => {
await searchPage.swipeLeft();
await page.waitForTimeout(500);
const searchInput = await page.$('.search-input input');
const isVisible = await searchInput?.isVisible();
expect(isVisible).toBe(true);
});
test('应该允许右滑', async ({ page }) => {
await searchPage.swipeRight();
await page.waitForTimeout(500);
const searchInput = await page.$('.search-input input');
const isVisible = await searchInput?.isVisible();
expect(isVisible).toBe(true);
});
test('应该允许上滑', async ({ page }) => {
await searchPage.swipeUp();
await page.waitForTimeout(500);
const searchInput = await page.$('.search-input input');
const isVisible = await searchInput?.isVisible();
expect(isVisible).toBe(true);
});
test('应该允许下滑', async ({ page }) => {
await searchPage.swipeDown();
await page.waitForTimeout(500);
const searchInput = await page.$('.search-input input');
const isVisible = await searchInput?.isVisible();
expect(isVisible).toBe(true);
});
test('应该显示搜索历史', async () => {
const searchHistory = await searchPage.getSearchHistory();
expect(Array.isArray(searchHistory)).toBe(true);
});
test('应该允许清除搜索历史', async () => {
await searchPage.clearSearchHistory();
const searchHistory = await searchPage.getSearchHistory();
expect(searchHistory.length).toBe(0);
});
test('应该显示热门搜索', async () => {
const hotSearches = await searchPage.getHotSearches();
expect(Array.isArray(hotSearches)).toBe(true);
});
test('应该允许点击热门搜索', async () => {
await searchPage.tapHotSearch(0);
await page.waitForTimeout(500);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
});
@@ -0,0 +1,150 @@
import { test, expect } from '@playwright/test';
import { MiniProgramUserPage } from '../pages/UserPage';
test.describe('小程序用户测试', () => {
let userPage: MiniProgramUserPage;
test.beforeEach(async ({ page }) => {
userPage = new MiniProgramUserPage(page);
await userPage.navigate();
});
test('应该显示用户名', async () => {
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
test('应该显示用户头像', async () => {
const avatar = await userPage.getUserAvatar();
expect(avatar).toBeTruthy();
});
test('应该检查登录状态', async () => {
const isLoggedIn = await userPage.isLoggedIn();
expect(typeof isLoggedIn).toBe('boolean');
});
test('应该允许登录', async () => {
await userPage.navigate();
const isLoggedInBefore = await userPage.isLoggedIn();
if (!isLoggedInBefore) {
await userPage.login('test-user', 'test-password');
const isLoggedInAfter = await userPage.isLoggedIn();
expect(isLoggedInAfter).toBe(true);
}
});
test('应该允许登出', async () => {
await userPage.logout();
const isLoggedIn = await userPage.isLoggedIn();
expect(isLoggedIn).toBe(false);
});
test('应该能够导航到个人资料', async () => {
await userPage.login('test-user', 'test-password');
await userPage.navigateToProfile();
const profile = await userPage.getProfile();
expect(profile.username).toBe('test-user');
});
test('应该能够导航到设置', async () => {
await userPage.navigateToSettings();
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
test('应该能够导航到历史记录', async () => {
await userPage.navigateToHistory();
const history = await userPage.getHistory();
expect(Array.isArray(history)).toBe(true);
});
test('应该能够导航到收藏', async () => {
await userPage.navigateToFavorites();
const favorites = await userPage.getFavorites();
expect(Array.isArray(favorites)).toBe(true);
});
test('应该允许更新个人资料', async () => {
await userPage.updateProfile({
username: 'updated-user',
email: 'updated@example.com',
phone: '1234567890',
});
const profile = await userPage.getProfile();
expect(profile.username).toBe('updated-user');
});
test('应该允许切换主题', async () => {
const themeBefore = await userPage.getCurrentTheme();
await userPage.toggleTheme();
const themeAfter = await userPage.getCurrentTheme();
expect(themeBefore).not.toBe(themeAfter);
});
test('应该允许更新设置', async () => {
await userPage.updateSetting('theme', 'dark');
const settings = await userPage.getSettings();
expect(settings.theme).toContain('dark');
});
test('应该允许清除历史记录', async () => {
await userPage.clearHistory();
const history = await userPage.getHistory();
expect(history.length).toBe(0);
});
test('应该允许移除收藏', async () => {
await userPage.navigateToFavorites();
const favoritesBefore = await userPage.getFavorites();
if (favoritesBefore.length > 0) {
const firstFavoriteId = '1';
await userPage.removeFavorite(firstFavoriteId);
const favoritesAfter = await userPage.getFavorites();
expect(favoritesAfter.length).toBe(favoritesBefore.length - 1);
}
});
test('应该允许点击按钮', async () => {
await userPage.navigate();
await userPage.tapButton('profile-button');
const profile = await userPage.getProfile();
expect(profile.username).toBeTruthy();
});
test('应该允许长按按钮', async ({ page }) => {
await userPage.navigate();
await userPage.longPressButton('settings-button');
await page.waitForTimeout(1000);
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
test('应该允许上滑', async ({ page }) => {
await userPage.navigate();
await userPage.swipeUp();
await page.waitForTimeout(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
test('应该允许下滑', async ({ page }) => {
await userPage.navigate();
await userPage.swipeDown();
await page.waitForTimeout(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
test('应该显示用户信息', async () => {
const userInfo = await userPage.getUserInfo();
expect(userInfo).toBeTruthy();
});
test('应该显示导航项', async () => {
const navItems = await userPage.getNavigationItems();
expect(Array.isArray(navItems)).toBe(true);
});
});
@@ -0,0 +1,135 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileAlmanacPage } from '../pages/AlmanacPage';
describe('Android Almanac Tests', () => {
let almanacPage: MobileAlmanacPage;
before(async () => {
almanacPage = new MobileAlmanacPage(browser);
await almanacPage.navigate();
});
it('should display current date', async () => {
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
it('should display lunar date', async () => {
const lunarDate = await almanacPage.getLunarDate();
expect(lunarDate).toBeTruthy();
});
it('should display gan zhi', async () => {
const ganzhi = await almanacPage.getGanZhi();
expect(ganzhi).toBeTruthy();
});
it('should display zodiac', async () => {
const zodiac = await almanacPage.getZodiac();
expect(zodiac).toBeTruthy();
});
it('should display solar term', async () => {
const solarTerm = await almanacPage.getSolarTerm();
expect(solarTerm).toBeTruthy();
});
it('should display suitable activities', async () => {
const suitable = await almanacPage.getSuitable();
expect(suitable).toBeTruthy();
});
it('should display unsuitable activities', async () => {
const unsuitable = await almanacPage.getUnsuitable();
expect(unsuitable).toBeTruthy();
});
it('should display day info', async () => {
const dayInfo = await almanacPage.getDayInfo();
expect(dayInfo).toBeTruthy();
});
it('should allow date search', async () => {
const date = '2026-02-11';
await almanacPage.searchDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should navigate to next day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToNextDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should navigate to previous day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToPreviousDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should display today indicator', async () => {
const isToday = await almanacPage.isToday();
expect(isToday).toBe(true);
});
it('should display almanac details', async () => {
const details = await almanacPage.getAlmanacDetails();
expect(details).toBeTruthy();
});
it('should allow tap on date', async () => {
const date = '2026-02-11';
await almanacPage.tapDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should allow long press on date', async () => {
const date = '2026-02-11';
await almanacPage.longPressDate(date);
await browser.pause(1000);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should allow swipe left to next day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeLeft();
await browser.pause(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should allow swipe right to previous day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeRight();
await browser.pause(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should allow share almanac', async () => {
await almanacPage.shareAlmanac();
await browser.pause(1000);
const shareDialog = await browser.$('.share-dialog');
const isVisible = await shareDialog.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow bookmark date', async () => {
await almanacPage.bookmarkDate();
await browser.pause(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(true);
});
it('should allow unbookmark date', async () => {
await almanacPage.bookmarkDate();
await browser.pause(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(false);
});
});
@@ -0,0 +1,97 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileCalendarPage } from '../pages/CalendarPage';
describe('Android Calendar Tests', () => {
let calendarPage: MobileCalendarPage;
before(async () => {
calendarPage = new MobileCalendarPage(browser);
await calendarPage.navigate();
});
it('should display current date', async () => {
const currentDate = await calendarPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
it('should allow date selection', async () => {
await calendarPage.selectDate('2026-02-11');
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain('2026-02-11');
});
it('should navigate to next month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToNextMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should navigate to previous month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToPreviousMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should display today indicator', async () => {
const today = new Date();
const todayDate = today.toISOString().split('T')[0];
const isToday = await calendarPage.isToday(todayDate);
expect(isToday).toBe(true);
});
it('should display weekend indicators', async () => {
const weekendDate = '2026-02-15';
const isWeekend = await calendarPage.isWeekend(weekendDate);
expect(isWeekend).toBe(true);
});
it('should allow tap on date', async () => {
const date = '2026-02-11';
await calendarPage.tapDate(date);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
it('should allow long press on date', async () => {
const date = '2026-02-11';
await calendarPage.longPressDate(date);
await browser.pause(1000);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
it('should allow swipe left to next month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeLeft();
await browser.pause(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should allow swipe right to previous month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeRight();
await browser.pause(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should display calendar events', async () => {
const events = await calendarPage.getCalendarEvents();
expect(Array.isArray(events)).toBe(true);
});
it('should display event indicators for dates with events', async () => {
const dateWithEvent = '2026-02-11';
const hasEvent = await calendarPage.hasEvent(dateWithEvent);
expect(hasEvent).toBe(true);
});
it('should display all dates in current month', async () => {
const date = '2026-02-15';
const isVisible = await calendarPage.isDateVisible(date);
expect(isVisible).toBe(true);
});
});
@@ -0,0 +1,211 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileSearchPage } from '../pages/SearchPage';
describe('Android Search Tests', () => {
let searchPage: MobileSearchPage;
before(async () => {
searchPage = new MobileSearchPage(browser);
await searchPage.navigate();
});
it('should display search input', async () => {
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow search', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
it('should allow clear search', async () => {
await searchPage.search('test');
await searchPage.clearSearch();
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(false);
});
it('should display search results', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBeGreaterThan(0);
});
it('should display result count', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const count = await searchPage.getResultCount();
expect(count).toBeGreaterThan(0);
});
it('should display no results when no matches', async () => {
const keyword = 'xyz123';
await searchPage.search(keyword);
const noResults = await searchPage.noResults();
expect(noResults).toBe(true);
});
it('should allow tap on result', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.tapResult(0);
await browser.pause(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
it('should allow long press on result', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.longPressResult(0);
await browser.pause(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
it('should display result title', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const title = await searchPage.getResultTitle(0);
expect(title).toBeTruthy();
});
it('should display result description', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const description = await searchPage.getResultDescription(0);
expect(description).toBeTruthy();
});
it('should display result date', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const date = await searchPage.getResultDate(0);
expect(date).toBeTruthy();
});
it('should allow filter by type', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
it('should allow filter by date range', async () => {
const startDate = '2026-01-01';
const endDate = '2026-12-31';
await searchPage.filterByDate(startDate, endDate);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
it('should allow sort', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.sortBy('date');
const currentSort = await searchPage.getCurrentSort();
expect(currentSort).toContain('date');
});
it('should display active filters', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const activeFilters = await searchPage.getActiveFilters();
expect(Array.isArray(activeFilters)).toBe(true);
});
it('should allow clear filters', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
await searchPage.clearFilters();
const activeFilters = await searchPage.getActiveFilters();
expect(activeFilters.length).toBe(0);
});
it('should allow save search', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).toContain(keyword);
});
it('should display saved searches', async () => {
const savedSearches = await searchPage.getSavedSearches();
expect(Array.isArray(savedSearches)).toBe(true);
});
it('should allow delete saved search', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
await searchPage.deleteSavedSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).not.toContain(keyword);
});
it('should display recent searches', async () => {
const keyword = 'test';
await searchPage.search(keyword);
const recentSearches = await searchPage.getRecentSearches();
expect(Array.isArray(recentSearches)).toBe(true);
});
it('should allow clear recent searches', async () => {
await searchPage.clearRecentSearches();
const recentSearches = await searchPage.getRecentSearches();
expect(recentSearches.length).toBe(0);
});
it('should display search suggestions', async () => {
const keyword = '春';
const suggestions = await searchPage.getSearchSuggestions(keyword);
expect(Array.isArray(suggestions)).toBe(true);
});
it('should allow select suggestion', async () => {
const keyword = '春';
await searchPage.selectSuggestion(0);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
it('should allow swipe left', async () => {
await searchPage.swipeLeft();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe right', async () => {
await searchPage.swipeRight();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe up', async () => {
await searchPage.swipeUp();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe down', async () => {
await searchPage.swipeDown();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
});
@@ -0,0 +1,140 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileUserPage } from '../pages/UserPage';
describe('Android User Tests', () => {
let userPage: MobileUserPage;
before(async () => {
userPage = new MobileUserPage(browser);
await userPage.navigate();
});
it('should display username', async () => {
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
it('should display user avatar', async () => {
const avatar = await userPage.getUserAvatar();
expect(avatar).toBeTruthy();
});
it('should check login status', async () => {
const isLoggedIn = await userPage.isLoggedIn();
expect(typeof isLoggedIn).toBe('boolean');
});
it('should allow login', async () => {
await userPage.navigate();
const isLoggedInBefore = await userPage.isLoggedIn();
if (!isLoggedInBefore) {
await userPage.login('test-user', 'test-password');
const isLoggedInAfter = await userPage.isLoggedIn();
expect(isLoggedInAfter).toBe(true);
}
});
it('should allow logout', async () => {
await userPage.logout();
const isLoggedIn = await userPage.isLoggedIn();
expect(isLoggedIn).toBe(false);
});
it('should navigate to profile', async () => {
await userPage.login('test-user', 'test-password');
await userPage.navigateToProfile();
const profile = await userPage.getProfile();
expect(profile.username).toBe('test-user');
});
it('should navigate to settings', async () => {
await userPage.navigateToSettings();
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
it('should navigate to history', async () => {
await userPage.navigateToHistory();
const history = await userPage.getHistory();
expect(Array.isArray(history)).toBe(true);
});
it('should navigate to favorites', async () => {
await userPage.navigateToFavorites();
const favorites = await userPage.getFavorites();
expect(Array.isArray(favorites)).toBe(true);
});
it('should allow profile update', async () => {
await userPage.updateProfile({
username: 'updated-user',
email: 'updated@example.com',
phone: '1234567890',
});
const profile = await userPage.getProfile();
expect(profile.username).toBe('updated-user');
});
it('should allow theme toggle', async () => {
const themeBefore = await userPage.getCurrentTheme();
await userPage.toggleTheme();
const themeAfter = await userPage.getCurrentTheme();
expect(themeBefore).not.toBe(themeAfter);
});
it('should allow settings update', async () => {
await userPage.updateSetting('theme', 'dark');
const settings = await userPage.getSettings();
expect(settings.theme).toContain('dark');
});
it('should allow history clear', async () => {
await userPage.clearHistory();
const history = await userPage.getHistory();
expect(history.length).toBe(0);
});
it('should allow favorite removal', async () => {
await userPage.navigateToFavorites();
const favoritesBefore = await userPage.getFavorites();
if (favoritesBefore.length > 0) {
const firstFavoriteId = '1';
await userPage.removeFavorite(firstFavoriteId);
const favoritesAfter = await userPage.getFavorites();
expect(favoritesAfter.length).toBe(favoritesBefore.length - 1);
}
});
it('should allow tap on button', async () => {
await userPage.navigate();
await userPage.tapButton('profile-button');
const profile = await userPage.getProfile();
expect(profile.username).toBeTruthy();
});
it('should allow long press on button', async () => {
await userPage.navigate();
await userPage.longPressButton('settings-button');
await browser.pause(1000);
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
it('should allow swipe up', async () => {
await userPage.navigate();
await userPage.swipeUp();
await browser.pause(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
it('should allow swipe down', async () => {
await userPage.navigate();
await userPage.swipeDown();
await browser.pause(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
});
@@ -0,0 +1,135 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileAlmanacPage } from '../pages/AlmanacPage';
describe('iOS Almanac Tests', () => {
let almanacPage: MobileAlmanacPage;
before(async () => {
almanacPage = new MobileAlmanacPage(browser);
await almanacPage.navigate();
});
it('should display current date', async () => {
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
it('should display lunar date', async () => {
const lunarDate = await almanacPage.getLunarDate();
expect(lunarDate).toBeTruthy();
});
it('should display gan zhi', async () => {
const ganzhi = await almanacPage.getGanZhi();
expect(ganzhi).toBeTruthy();
});
it('should display zodiac', async () => {
const zodiac = await almanacPage.getZodiac();
expect(zodiac).toBeTruthy();
});
it('should display solar term', async () => {
const solarTerm = await almanacPage.getSolarTerm();
expect(solarTerm).toBeTruthy();
});
it('should display suitable activities', async () => {
const suitable = await almanacPage.getSuitable();
expect(suitable).toBeTruthy();
});
it('should display unsuitable activities', async () => {
const unsuitable = await almanacPage.getUnsuitable();
expect(unsuitable).toBeTruthy();
});
it('should display day info', async () => {
const dayInfo = await almanacPage.getDayInfo();
expect(dayInfo).toBeTruthy();
});
it('should allow date search', async () => {
const date = '2026-02-11';
await almanacPage.searchDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should navigate to next day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToNextDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should navigate to previous day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.navigateToPreviousDay();
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should display today indicator', async () => {
const isToday = await almanacPage.isToday();
expect(isToday).toBe(true);
});
it('should display almanac details', async () => {
const details = await almanacPage.getAlmanacDetails();
expect(details).toBeTruthy();
});
it('should allow tap on date', async () => {
const date = '2026-02-11';
await almanacPage.tapDate(date);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should allow long press on date', async () => {
const date = '2026-02-11';
await almanacPage.longPressDate(date);
await browser.pause(1000);
const currentDate = await almanacPage.getCurrentDate();
expect(currentDate).toContain(date);
});
it('should allow swipe left to next day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeLeft();
await browser.pause(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should allow swipe right to previous day', async () => {
const currentDateBefore = await almanacPage.getCurrentDate();
await almanacPage.swipeRight();
await browser.pause(500);
const currentDateAfter = await almanacPage.getCurrentDate();
expect(currentDateBefore).not.toBe(currentDateAfter);
});
it('should allow share almanac', async () => {
await almanacPage.shareAlmanac();
await browser.pause(1000);
const shareDialog = await browser.$('.share-dialog');
const isVisible = await shareDialog.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow bookmark date', async () => {
await almanacPage.bookmarkDate();
await browser.pause(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(true);
});
it('should allow unbookmark date', async () => {
await almanacPage.bookmarkDate();
await browser.pause(500);
const isBookmarked = await almanacPage.isBookmarked();
expect(isBookmarked).toBe(false);
});
});
@@ -0,0 +1,97 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileCalendarPage } from '../pages/CalendarPage';
describe('iOS Calendar Tests', () => {
let calendarPage: MobileCalendarPage;
before(async () => {
calendarPage = new MobileCalendarPage(browser);
await calendarPage.navigate();
});
it('should display current date', async () => {
const currentDate = await calendarPage.getCurrentDate();
expect(currentDate).toBeTruthy();
});
it('should allow date selection', async () => {
await calendarPage.selectDate('2026-02-11');
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain('2026-02-11');
});
it('should navigate to next month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToNextMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should navigate to previous month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.navigateToPreviousMonth();
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should display today indicator', async () => {
const today = new Date();
const todayDate = today.toISOString().split('T')[0];
const isToday = await calendarPage.isToday(todayDate);
expect(isToday).toBe(true);
});
it('should display weekend indicators', async () => {
const weekendDate = '2026-02-15';
const isWeekend = await calendarPage.isWeekend(weekendDate);
expect(isWeekend).toBe(true);
});
it('should allow tap on date', async () => {
const date = '2026-02-11';
await calendarPage.tapDate(date);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
it('should allow long press on date', async () => {
const date = '2026-02-11';
await calendarPage.longPressDate(date);
await browser.pause(1000);
const selectedDate = await calendarPage.getSelectedDate();
expect(selectedDate).toContain(date);
});
it('should allow swipe left to next month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeLeft();
await browser.pause(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should allow swipe right to previous month', async () => {
const currentMonthBefore = await calendarPage.getCurrentMonth();
await calendarPage.swipeRight();
await browser.pause(500);
const currentMonthAfter = await calendarPage.getCurrentMonth();
expect(currentMonthBefore).not.toBe(currentMonthAfter);
});
it('should display calendar events', async () => {
const events = await calendarPage.getCalendarEvents();
expect(Array.isArray(events)).toBe(true);
});
it('should display event indicators for dates with events', async () => {
const dateWithEvent = '2026-02-11';
const hasEvent = await calendarPage.hasEvent(dateWithEvent);
expect(hasEvent).toBe(true);
});
it('should display all dates in current month', async () => {
const date = '2026-02-15';
const isVisible = await calendarPage.isDateVisible(date);
expect(isVisible).toBe(true);
});
});
@@ -0,0 +1,211 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileSearchPage } from '../pages/SearchPage';
describe('iOS Search Tests', () => {
let searchPage: MobileSearchPage;
before(async () => {
searchPage = new MobileSearchPage(browser);
await searchPage.navigate();
});
it('should display search input', async () => {
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow search', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
it('should allow clear search', async () => {
await searchPage.search('test');
await searchPage.clearSearch();
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(false);
});
it('should display search results', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
expect(results.length).toBeGreaterThan(0);
});
it('should display result count', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const count = await searchPage.getResultCount();
expect(count).toBeGreaterThan(0);
});
it('should display no results when no matches', async () => {
const keyword = 'xyz123';
await searchPage.search(keyword);
const noResults = await searchPage.noResults();
expect(noResults).toBe(true);
});
it('should allow tap on result', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.tapResult(0);
await browser.pause(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
it('should allow long press on result', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.longPressResult(0);
await browser.pause(1000);
const resultTitle = await searchPage.getResultTitle(0);
expect(resultTitle).toBeTruthy();
});
it('should display result title', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const title = await searchPage.getResultTitle(0);
expect(title).toBeTruthy();
});
it('should display result description', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const description = await searchPage.getResultDescription(0);
expect(description).toBeTruthy();
});
it('should display result date', async () => {
const keyword = '春节';
await searchPage.search(keyword);
const date = await searchPage.getResultDate(0);
expect(date).toBeTruthy();
});
it('should allow filter by type', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
it('should allow filter by date range', async () => {
const startDate = '2026-01-01';
const endDate = '2026-12-31';
await searchPage.filterByDate(startDate, endDate);
const results = await searchPage.getSearchResults();
expect(Array.isArray(results)).toBe(true);
});
it('should allow sort', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.sortBy('date');
const currentSort = await searchPage.getCurrentSort();
expect(currentSort).toContain('date');
});
it('should display active filters', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
const activeFilters = await searchPage.getActiveFilters();
expect(Array.isArray(activeFilters)).toBe(true);
});
it('should allow clear filters', async () => {
const keyword = '春节';
await searchPage.search(keyword);
await searchPage.filterByType('holiday');
await searchPage.clearFilters();
const activeFilters = await searchPage.getActiveFilters();
expect(activeFilters.length).toBe(0);
});
it('should allow save search', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).toContain(keyword);
});
it('should display saved searches', async () => {
const savedSearches = await searchPage.getSavedSearches();
expect(Array.isArray(savedSearches)).toBe(true);
});
it('should allow delete saved search', async () => {
const keyword = '春节';
await searchPage.saveSearch(keyword);
await searchPage.deleteSavedSearch(keyword);
const savedSearches = await searchPage.getSavedSearches();
expect(savedSearches).not.toContain(keyword);
});
it('should display recent searches', async () => {
const keyword = 'test';
await searchPage.search(keyword);
const recentSearches = await searchPage.getRecentSearches();
expect(Array.isArray(recentSearches)).toBe(true);
});
it('should allow clear recent searches', async () => {
await searchPage.clearRecentSearches();
const recentSearches = await searchPage.getRecentSearches();
expect(recentSearches.length).toBe(0);
});
it('should display search suggestions', async () => {
const keyword = '春';
const suggestions = await searchPage.getSearchSuggestions(keyword);
expect(Array.isArray(suggestions)).toBe(true);
});
it('should allow select suggestion', async () => {
const keyword = '春';
await searchPage.selectSuggestion(0);
const hasResults = await searchPage.hasResults();
expect(hasResults).toBe(true);
});
it('should allow swipe left', async () => {
await searchPage.swipeLeft();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe right', async () => {
await searchPage.swipeRight();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe up', async () => {
await searchPage.swipeUp();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
it('should allow swipe down', async () => {
await searchPage.swipeDown();
await browser.pause(500);
const searchInput = await browser.$('.search-input input');
const isVisible = await searchInput.isDisplayed();
expect(isVisible).toBe(true);
});
});
@@ -0,0 +1,140 @@
import { describe, it, expect } from '@wdio/globals';
import { MobileUserPage } from '../pages/UserPage';
describe('iOS User Tests', () => {
let userPage: MobileUserPage;
before(async () => {
userPage = new MobileUserPage(browser);
await userPage.navigate();
});
it('should display username', async () => {
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
it('should display user avatar', async () => {
const avatar = await userPage.getUserAvatar();
expect(avatar).toBeTruthy();
});
it('should check login status', async () => {
const isLoggedIn = await userPage.isLoggedIn();
expect(typeof isLoggedIn).toBe('boolean');
});
it('should allow login', async () => {
await userPage.navigate();
const isLoggedInBefore = await userPage.isLoggedIn();
if (!isLoggedInBefore) {
await userPage.login('test-user', 'test-password');
const isLoggedInAfter = await userPage.isLoggedIn();
expect(isLoggedInAfter).toBe(true);
}
});
it('should allow logout', async () => {
await userPage.logout();
const isLoggedIn = await userPage.isLoggedIn();
expect(isLoggedIn).toBe(false);
});
it('should navigate to profile', async () => {
await userPage.login('test-user', 'test-password');
await userPage.navigateToProfile();
const profile = await userPage.getProfile();
expect(profile.username).toBe('test-user');
});
it('should navigate to settings', async () => {
await userPage.navigateToSettings();
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
it('should navigate to history', async () => {
await userPage.navigateToHistory();
const history = await userPage.getHistory();
expect(Array.isArray(history)).toBe(true);
});
it('should navigate to favorites', async () => {
await userPage.navigateToFavorites();
const favorites = await userPage.getFavorites();
expect(Array.isArray(favorites)).toBe(true);
});
it('should allow profile update', async () => {
await userPage.updateProfile({
username: 'updated-user',
email: 'updated@example.com',
phone: '1234567890',
});
const profile = await userPage.getProfile();
expect(profile.username).toBe('updated-user');
});
it('should allow theme toggle', async () => {
const themeBefore = await userPage.getCurrentTheme();
await userPage.toggleTheme();
const themeAfter = await userPage.getCurrentTheme();
expect(themeBefore).not.toBe(themeAfter);
});
it('should allow settings update', async () => {
await userPage.updateSetting('theme', 'dark');
const settings = await userPage.getSettings();
expect(settings.theme).toContain('dark');
});
it('should allow history clear', async () => {
await userPage.clearHistory();
const history = await userPage.getHistory();
expect(history.length).toBe(0);
});
it('should allow favorite removal', async () => {
await userPage.navigateToFavorites();
const favoritesBefore = await userPage.getFavorites();
if (favoritesBefore.length > 0) {
const firstFavoriteId = '1';
await userPage.removeFavorite(firstFavoriteId);
const favoritesAfter = await userPage.getFavorites();
expect(favoritesAfter.length).toBe(favoritesBefore.length - 1);
}
});
it('should allow tap on button', async () => {
await userPage.navigate();
await userPage.tapButton('profile-button');
const profile = await userPage.getProfile();
expect(profile.username).toBeTruthy();
});
it('should allow long press on button', async () => {
await userPage.navigate();
await userPage.longPressButton('settings-button');
await browser.pause(1000);
const settings = await userPage.getSettings();
expect(settings.theme).toBeTruthy();
});
it('should allow swipe up', async () => {
await userPage.navigate();
await userPage.swipeUp();
await browser.pause(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
it('should allow swipe down', async () => {
await userPage.navigate();
await userPage.swipeDown();
await browser.pause(500);
const username = await userPage.getUsername();
expect(username).toBeTruthy();
});
});
@@ -0,0 +1,107 @@
import { Page } from 'playwright';
export class MobileAlmanacPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:8081/#/pages/almanac/index');
await this.page.waitForLoadState('networkidle');
}
async getCurrentDate() {
const currentDate = await this.page.locator('.current-date').textContent();
return currentDate || '';
}
async getLunarDate() {
const lunarDate = await this.page.locator('.lunar-date').textContent();
return lunarDate || '';
}
async getGanZhi() {
const ganzhi = await this.page.locator('.ganzhi').textContent();
return ganzhi || '';
}
async getZodiac() {
const zodiac = await this.page.locator('.zodiac').textContent();
return zodiac || '';
}
async getSolarTerm() {
const solarTerm = await this.page.locator('.solar-term').textContent();
return solarTerm || '';
}
async getSuitable() {
const suitable = await this.page.locator('.suitable').textContent();
return suitable || '';
}
async getUnsuitable() {
const unsuitable = await this.page.locator('.unsuitable').textContent();
return unsuitable || '';
}
async getDayInfo() {
const dayInfo = await this.page.locator('.day-info').textContent();
return dayInfo || '';
}
async searchDate(date: string) {
await this.page.fill('.date-search input', date);
await this.page.click('.date-search button');
await this.page.waitForLoadState('networkidle');
}
async navigateToNextDay() {
await this.page.click('.next-day');
}
async navigateToPreviousDay() {
await this.page.click('.previous-day');
}
async isToday() {
const todayIndicator = this.page.locator('.today-indicator');
return await todayIndicator.isVisible();
}
async getAlmanacDetails() {
const details = await this.page.locator('.almanac-details').textContent();
return details || '';
}
async tapDate(date: string) {
await this.page.tap(`[data-date="${date}"]`);
}
async longPressDate(date: string) {
const element = this.page.locator(`[data-date="${date}"]`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
async shareAlmanac() {
await this.page.click('.share-button');
}
async bookmarkDate() {
await this.page.click('.bookmark-button');
}
async isBookmarked() {
const bookmarked = this.page.locator('.bookmarked');
return await bookmarked.isVisible();
}
}
@@ -0,0 +1,82 @@
import { Page } from 'playwright';
export class MobileCalendarPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:8081/#/pages/calendar/index');
await this.page.waitForLoadState('networkidle');
}
async getCurrentDate() {
const currentDate = await this.page.locator('.current-date').textContent();
return currentDate || '';
}
async selectDate(date: string) {
await this.page.click(`[data-date="${date}"]`);
}
async getSelectedDate() {
const selectedDate = await this.page.locator('.selected-date').textContent();
return selectedDate || '';
}
async navigateToNextMonth() {
await this.page.click('.next-month');
}
async navigateToPreviousMonth() {
await this.page.click('.previous-month');
}
async getCurrentMonth() {
const currentMonth = await this.page.locator('.current-month').textContent();
return currentMonth || '';
}
async isDateVisible(date: string) {
const dateElement = this.page.locator(`[data-date="${date}"]`);
return await dateElement.isVisible();
}
async isToday(date: string) {
const todayElement = this.page.locator(`[data-date="${date}"].today`);
return await todayElement.isVisible();
}
async isWeekend(date: string) {
const weekendElement = this.page.locator(`[data-date="${date}"].weekend`);
return await weekendElement.isVisible();
}
async getCalendarEvents() {
const events = await this.page.locator('.calendar-event').allTextContents();
return events;
}
async hasEvent(date: string) {
const eventIndicator = this.page.locator(`[data-date="${date}"] .event-indicator`);
return await eventIndicator.isVisible();
}
async tapDate(date: string) {
await this.page.tap(`[data-date="${date}"]`);
}
async longPressDate(date: string) {
const element = this.page.locator(`[data-date="${date}"]`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
}
@@ -0,0 +1,158 @@
import { Page } from 'playwright';
export class MobileSearchPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:8081/#/pages/search/index');
await this.page.waitForLoadState('networkidle');
}
async search(keyword: string) {
await this.page.fill('.search-input input', keyword);
await this.page.click('.search-button');
await this.page.waitForLoadState('networkidle');
}
async clearSearch() {
await this.page.click('.clear-button');
await this.page.waitForLoadState('networkidle');
}
async getSearchResults() {
const results = await this.page.locator('.search-result').allTextContents();
return results;
}
async getResultCount() {
const count = await this.page.locator('.search-result').count();
return count;
}
async hasResults() {
const count = await this.getResultCount();
return count > 0;
}
async noResults() {
const noResults = this.page.locator('.no-results');
return await noResults.isVisible();
}
async tapResult(index: number) {
await this.page.tap(`.search-result:nth-child(${index + 1})`);
}
async longPressResult(index: number) {
const element = this.page.locator(`.search-result:nth-child(${index + 1})`);
await element.tap();
await this.page.waitForTimeout(500);
}
async getResultTitle(index: number) {
const title = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-title`).textContent();
return title || '';
}
async getResultDescription(index: number) {
const description = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-description`).textContent();
return description || '';
}
async getResultDate(index: number) {
const date = await this.page.locator(`.search-result:nth-child(${index + 1}) .result-date`).textContent();
return date || '';
}
async filterByType(type: string) {
await this.page.click(`.filter-type[data-type="${type}"]`);
await this.page.waitForLoadState('networkidle');
}
async filterByDate(startDate: string, endDate: string) {
await this.page.click('.filter-date');
await this.page.fill('.filter-start-date input', startDate);
await this.page.fill('.filter-end-date input', endDate);
await this.page.click('.apply-filter');
await this.page.waitForLoadState('networkidle');
}
async sortBy(sortType: string) {
await this.page.click('.sort-button');
await this.page.click(`.sort-option[data-sort="${sortType}"]`);
await this.page.waitForLoadState('networkidle');
}
async getCurrentSort() {
const currentSort = await this.page.locator('.current-sort').textContent();
return currentSort || '';
}
async getActiveFilters() {
const activeFilters = await this.page.locator('.active-filter').allTextContents();
return activeFilters;
}
async clearFilters() {
await this.page.click('.clear-filters');
await this.page.waitForLoadState('networkidle');
}
async saveSearch(keyword: string) {
await this.search(keyword);
await this.page.click('.save-search-button');
await this.page.waitForLoadState('networkidle');
}
async getSavedSearches() {
const savedSearches = await this.page.locator('.saved-search').allTextContents();
return savedSearches;
}
async deleteSavedSearch(keyword: string) {
await this.page.click(`.saved-search[data-keyword="${keyword}"] .delete-button`);
await this.page.waitForLoadState('networkidle');
}
async getRecentSearches() {
const recentSearches = await this.page.locator('.recent-search').allTextContents();
return recentSearches;
}
async clearRecentSearches() {
await this.page.click('.clear-recent');
await this.page.waitForLoadState('networkidle');
}
async getSearchSuggestions(keyword: string) {
await this.page.fill('.search-input input', keyword);
await this.page.waitForTimeout(500);
const suggestions = await this.page.locator('.search-suggestion').allTextContents();
return suggestions;
}
async selectSuggestion(index: number) {
await this.page.tap(`.search-suggestion:nth-child(${index + 1})`);
await this.page.waitForLoadState('networkidle');
}
async swipeLeft() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(100, 0);
}
async swipeRight() {
await this.page.touchscreen.tap(100, 0);
await this.page.touchscreen.tap(0, 0);
}
async swipeUp() {
await this.page.touchscreen.tap(0, 100);
await this.page.touchscreen.tap(0, 0);
}
async swipeDown() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(0, 100);
}
}
@@ -0,0 +1,152 @@
import { Page } from 'playwright';
export class MobileUserPage {
constructor(private page: Page) {}
async navigate() {
await this.page.goto('http://localhost:8081/#/pages/user/index');
await this.page.waitForLoadState('networkidle');
}
async getUsername() {
const username = await this.page.locator('.username').textContent();
return username || '';
}
async getUserAvatar() {
const avatar = await this.page.locator('.user-avatar');
return await avatar.getAttribute('src');
}
async isLoggedIn() {
const loginButton = this.page.locator('.login-button');
return !(await loginButton.isVisible());
}
async login(username: string, password: string) {
await this.page.fill('.login-username input', username);
await this.page.fill('.login-password input', password);
await this.page.click('.login-button');
await this.page.waitForLoadState('networkidle');
}
async logout() {
await this.page.click('.logout-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToProfile() {
await this.page.click('.profile-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToSettings() {
await this.page.click('.settings-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToHistory() {
await this.page.click('.history-button');
await this.page.waitForLoadState('networkidle');
}
async navigateToFavorites() {
await this.page.click('.favorites-button');
await this.page.waitForLoadState('networkidle');
}
async updateProfile(data: { username?: string; email?: string; phone?: string }) {
await this.navigateToProfile();
if (data.username) {
await this.page.fill('.profile-username input', data.username);
}
if (data.email) {
await this.page.fill('.profile-email input', data.email);
}
if (data.phone) {
await this.page.fill('.profile-phone input', data.phone);
}
await this.page.click('.save-button');
await this.page.waitForLoadState('networkidle');
}
async getProfile() {
const profile = {
username: await this.page.locator('.profile-username').textContent() || '',
email: await this.page.locator('.profile-email').textContent() || '',
phone: await this.page.locator('.profile-phone').textContent() || '',
};
return profile;
}
async toggleTheme() {
await this.page.click('.theme-toggle');
await this.page.waitForTimeout(500);
}
async getCurrentTheme() {
const theme = await this.page.locator('.current-theme').textContent();
return theme || '';
}
async getSettings() {
const settings = {
theme: await this.page.locator('.setting-theme').textContent() || '',
language: await this.page.locator('.setting-language').textContent() || '',
notifications: await this.page.locator('.setting-notifications').textContent() || '',
};
return settings;
}
async updateSetting(key: string, value: string) {
await this.navigateToSettings();
await this.page.click(`.setting-${key}`);
await this.page.click(`[data-value="${value}"]`);
await this.page.click('.save-button');
await this.page.waitForLoadState('networkidle');
}
async getHistory() {
const historyItems = await this.page.locator('.history-item').allTextContents();
return historyItems;
}
async clearHistory() {
await this.navigateToHistory();
await this.page.click('.clear-history-button');
await this.page.waitForLoadState('networkidle');
}
async getFavorites() {
const favorites = await this.page.locator('.favorite-item').allTextContents();
return favorites;
}
async removeFavorite(id: string) {
await this.navigateToFavorites();
await this.page.click(`[data-favorite-id="${id}"] .remove-button`);
await this.page.waitForLoadState('networkidle');
}
async tapButton(buttonClass: string) {
await this.page.tap(`.${buttonClass}`);
}
async longPressButton(buttonClass: string) {
const element = this.page.locator(`.${buttonClass}`);
await element.tap();
await this.page.waitForTimeout(500);
}
async swipeUp() {
await this.page.touchscreen.tap(0, 100);
await this.page.touchscreen.tap(0, 0);
}
async swipeDown() {
await this.page.touchscreen.tap(0, 0);
await this.page.touchscreen.tap(0, 100);
}
}
@@ -0,0 +1,4 @@
export { MobileCalendarPage } from './CalendarPage';
export { MobileAlmanacPage } from './AlmanacPage';
export { MobileUserPage } from './UserPage';
export { MobileSearchPage } from './SearchPage';
@@ -0,0 +1,59 @@
import { Page, Locator } from '@playwright/test';
export class AlmanacPage {
readonly page: Page;
readonly themeSwitch: Locator;
readonly almanacCard: Locator;
readonly suitableText: Locator;
readonly avoidText: Locator;
readonly navigationBar: Locator;
constructor(page: Page) {
this.page = page;
this.themeSwitch = page.locator('[data-testid="theme-switch"], .theme-switch');
this.almanacCard = page.locator('[data-testid="almanac-card"], .almanac-card');
this.suitableText = page.locator('[data-testid="suitable"], .suitable');
this.avoidText = page.locator('[data-testid="avoid"], .avoid');
this.navigationBar = page.locator('.uni-tabbar, [class*="tabbar"]');
}
async goto() {
await this.page.goto('/#/pages/almanac/index');
await this.page.waitForLoadState('networkidle');
}
async switchTheme(themeId: string) {
await this.themeSwitch.click();
await this.page.locator(`[data-testid="theme-${themeId}"], [data-theme="${themeId}"]`).click();
await this.page.waitForTimeout(500);
}
async getCurrentTheme(): Promise<string | null> {
return await this.page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return style.getPropertyValue('--theme-id') || null;
});
}
async isAlmanacVisible(): Promise<boolean> {
return await this.almanacCard.isVisible().catch(() => false);
}
async getSuitableText(): Promise<string | null> {
return await this.suitableText.textContent().catch(() => null);
}
async getAvoidText(): Promise<string | null> {
return await this.avoidText.textContent().catch(() => null);
}
async navigateToTab(tabName: string) {
await this.navigationBar.locator(`text=${tabName}`).click();
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(path: string) {
await this.page.screenshot({ path, fullPage: true });
}
}
@@ -0,0 +1,53 @@
import { Page, Locator } from '@playwright/test';
export class CalendarPage {
readonly page: Page;
readonly themeSwitch: Locator;
readonly calendarGrid: Locator;
readonly currentDate: Locator;
readonly navigationBar: Locator;
constructor(page: Page) {
this.page = page;
this.themeSwitch = page.locator('[data-testid="theme-switch"], .theme-switch');
this.calendarGrid = page.locator('[data-testid="calendar-grid"], .calendar-grid');
this.currentDate = page.locator('[data-testid="current-date"], .current-date');
this.navigationBar = page.locator('.uni-tabbar, [class*="tabbar"]');
}
async goto() {
await this.page.goto('/#/pages/calendar/index');
await this.page.waitForLoadState('networkidle');
}
async switchTheme(themeId: string) {
await this.themeSwitch.click();
await this.page.locator(`[data-testid="theme-${themeId}"], [data-theme="${themeId}"]`).click();
await this.page.waitForTimeout(500);
}
async getCurrentTheme(): Promise<string | null> {
return await this.page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return style.getPropertyValue('--theme-id') || null;
});
}
async isCalendarVisible(): Promise<boolean> {
return await this.calendarGrid.isVisible().catch(() => false);
}
async getCurrentDateText(): Promise<string | null> {
return await this.currentDate.textContent().catch(() => null);
}
async navigateToTab(tabName: string) {
await this.navigationBar.locator(`text=${tabName}`).click();
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(path: string) {
await this.page.screenshot({ path, fullPage: true });
}
}
@@ -0,0 +1,58 @@
import { Page, Locator } from '@playwright/test';
export class SearchPage {
readonly page: Page;
readonly searchInput: Locator;
readonly searchButton: Locator;
readonly searchResults: Locator;
readonly searchHistory: Locator;
readonly navigationBar: Locator;
constructor(page: Page) {
this.page = page;
this.searchInput = page.locator('[data-testid="search-input"], .search-input, input[type="search"]');
this.searchButton = page.locator('[data-testid="search-button"], .search-button, button[type="submit"]');
this.searchResults = page.locator('[data-testid="search-results"], .search-results');
this.searchHistory = page.locator('[data-testid="search-history"], .search-history');
this.navigationBar = page.locator('.uni-tabbar, [class*="tabbar"]');
}
async goto() {
await this.page.goto('/#/pages/almanac-search/index');
await this.page.waitForLoadState('networkidle');
}
async isSearchInputVisible(): Promise<boolean> {
return await this.searchInput.isVisible().catch(() => false);
}
async fillSearchInput(text: string) {
await this.searchInput.fill(text);
}
async clickSearchButton() {
await this.searchButton.click();
await this.page.waitForLoadState('networkidle');
}
async getSearchResultsCount(): Promise<number> {
return await this.searchResults.locator('[data-testid="search-result-card"], .search-result-card').count();
}
async isSearchResultsVisible(): Promise<boolean> {
return await this.searchResults.isVisible().catch(() => false);
}
async isSearchHistoryVisible(): Promise<boolean> {
return await this.searchHistory.isVisible().catch(() => false);
}
async navigateToTab(tabName: string) {
await this.navigationBar.locator(`text=${tabName}`).click();
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(path: string) {
await this.page.screenshot({ path, fullPage: true });
}
}
@@ -0,0 +1,51 @@
import { Page, Locator } from '@playwright/test';
export class UserPage {
readonly page: Page;
readonly userInfoCard: Locator;
readonly loginButton: Locator;
readonly logoutButton: Locator;
readonly navigationBar: Locator;
constructor(page: Page) {
this.page = page;
this.userInfoCard = page.locator('[data-testid="user-info-card"], .user-info-card');
this.loginButton = page.locator('[data-testid="login-button"], .login-button');
this.logoutButton = page.locator('[data-testid="logout-button"], .logout-button');
this.navigationBar = page.locator('.uni-tabbar, [class*="tabbar"]');
}
async goto() {
await this.page.goto('/#/pages/user/index');
await this.page.waitForLoadState('networkidle');
}
async isUserInfoVisible(): Promise<boolean> {
return await this.userInfoCard.isVisible().catch(() => false);
}
async isLoginButtonVisible(): Promise<boolean> {
return await this.loginButton.isVisible().catch(() => false);
}
async isLogoutButtonVisible(): Promise<boolean> {
return await this.logoutButton.isVisible().catch(() => false);
}
async clickLogin() {
await this.loginButton.click();
}
async clickLogout() {
await this.logoutButton.click();
}
async navigateToTab(tabName: string) {
await this.navigationBar.locator(`text=${tabName}`).click();
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(path: string) {
await this.page.screenshot({ path, fullPage: true });
}
}
@@ -0,0 +1,4 @@
export { CalendarPage } from './CalendarPage';
export { AlmanacPage } from './AlmanacPage';
export { UserPage } from './UserPage';
export { SearchPage } from './SearchPage';
@@ -0,0 +1,425 @@
import { test, expect, Page } from '@playwright/test';
const ANIMATION_THRESHOLDS = {
fps: 30,
frameTime: 33.33,
animationDuration: 500,
};
interface AnimationMetrics {
fps: number;
frameTime: number;
droppedFrames: number;
totalFrames: number;
}
async function measureAnimationPerformance(page: Page, animationTrigger: () => Promise<void>, duration: number = 1000): Promise<AnimationMetrics> {
const frames: number[] = [];
await page.evaluate(() => {
window.performanceMetrics = {
frames: [],
startTime: performance.now(),
};
});
const startTime = Date.now();
const frameCollector = setInterval(async () => {
const timestamp = await page.evaluate(() => {
const metrics = (window as any).performanceMetrics;
if (metrics) {
metrics.frames.push(performance.now() - metrics.startTime);
}
return performance.now();
});
frames.push(timestamp);
}, 16);
await animationTrigger();
await page.waitForTimeout(duration);
clearInterval(frameCollector);
const metrics = await page.evaluate(() => {
const metrics = (window as any).performanceMetrics;
return metrics ? metrics.frames : [];
});
const totalFrames = metrics.length;
const droppedFrames = metrics.filter((frameTime: number, index: number) => {
if (index === 0) return false;
return frameTime - metrics[index - 1] > 33.33;
}).length;
const fps = totalFrames / (duration / 1000);
const frameTime = duration / totalFrames;
return {
fps,
frameTime,
droppedFrames,
totalFrames,
};
}
async function measureThemeSwitchAnimation(page: Page): Promise<AnimationMetrics> {
return await measureAnimationPerformance(page, async () => {
await page.click('.theme-switch-btn');
}, 500);
}
async function measurePageTransitionAnimation(page: Page, targetUrl: string): Promise<AnimationMetrics> {
return await measureAnimationPerformance(page, async () => {
await page.click(`[data-href="${targetUrl}"]`);
}, 500);
}
async function measureComponentAnimation(page: Page, selector: string): Promise<AnimationMetrics> {
return await measureAnimationPerformance(page, async () => {
await page.click(selector);
}, 500);
}
test.describe('主题切换动画性能测试', () => {
test('浅色主题切换到深色主题FPS应大于30', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.theme-switch-btn');
const metrics = await measureThemeSwitchAnimation(page);
console.log('主题切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('深色主题切换到浅色主题FPS应大于30', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.theme-switch-btn');
await page.click('.theme-switch-btn');
await page.waitForTimeout(500);
const metrics = await measureThemeSwitchAnimation(page);
console.log('主题切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('主题切换掉帧数应小于5', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.theme-switch-btn');
const metrics = await measureThemeSwitchAnimation(page);
console.log('主题切换掉帧数:', metrics.droppedFrames);
expect(metrics.droppedFrames).toBeLessThan(5);
});
test('主题切换帧时间应小于33.33ms', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.theme-switch-btn');
const metrics = await measureThemeSwitchAnimation(page);
console.log('主题切换帧时间:', metrics.frameTime);
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
});
});
test.describe('页面切换动画性能测试', () => {
test('首页切换到日历页FPS应大于30', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/calendar/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('首页切换到黄历页FPS应大于30', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/almanac/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/almanac/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('首页切换到用户页FPS应大于30', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/user/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/user/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('首页切换到搜索页FPS应大于30', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/search/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/search/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('日历页切换到黄历页FPS应大于30', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('[data-href="/pages/almanac/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/almanac/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('黄历页切换到日历页FPS应大于30', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('[data-href="/pages/calendar/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
console.log('页面切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('页面切换掉帧数应小于5', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/calendar/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
console.log('页面切换掉帧数:', metrics.droppedFrames);
expect(metrics.droppedFrames).toBeLessThan(5);
});
test('页面切换帧时间应小于33.33ms', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('[data-href="/pages/calendar/index"]');
const metrics = await measurePageTransitionAnimation(page, '/pages/calendar/index');
console.log('页面切换帧时间:', metrics.frameTime);
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
});
});
test.describe('组件动画性能测试', () => {
test('日历月切换动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-next-month');
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
console.log('日历月切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('日历年切换动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-year-selector');
await page.click('.calendar-year-selector');
await page.waitForSelector('.calendar-year-option[data-year="2027"]');
const metrics = await measureComponentAnimation(page, '.calendar-year-option[data-year="2027"]');
console.log('日历年切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('黄历月切换动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('.almanac-next-month');
const metrics = await measureComponentAnimation(page, '.almanac-next-month');
console.log('黄历月切换动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('搜索建议展开动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
await page.fill('.search-input', '春');
await page.waitForSelector('.search-suggestions');
const metrics = await measureComponentAnimation(page, '.search-suggestions');
console.log('搜索建议展开动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('搜索历史展开动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-history-btn');
const metrics = await measureComponentAnimation(page, '.search-history-btn');
console.log('搜索历史展开动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('搜索热门展开动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-hot-btn');
const metrics = await measureComponentAnimation(page, '.search-hot-btn');
console.log('搜索热门展开动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('用户设置展开动画FPS应大于30', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.user-settings-btn');
const metrics = await measureComponentAnimation(page, '.user-settings-btn');
console.log('用户设置展开动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('组件动画掉帧数应小于5', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-next-month');
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
console.log('组件动画掉帧数:', metrics.droppedFrames);
expect(metrics.droppedFrames).toBeLessThan(5);
});
test('组件动画帧时间应小于33.33ms', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-next-month');
const metrics = await measureComponentAnimation(page, '.calendar-next-month');
console.log('组件动画帧时间:', metrics.frameTime);
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
});
});
test.describe('滚动动画性能测试', () => {
test('日历列表滚动FPS应大于30', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const metrics = await measureAnimationPerformance(page, async () => {
await page.evaluate(() => {
const container = document.querySelector('.calendar-container');
if (container) {
container.scrollTop = 500;
}
});
}, 500);
console.log('日历列表滚动动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('黄历列表滚动FPS应大于30', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('.almanac-container');
const metrics = await measureAnimationPerformance(page, async () => {
await page.evaluate(() => {
const container = document.querySelector('.almanac-container');
if (container) {
container.scrollTop = 500;
}
});
}, 500);
console.log('黄历列表滚动动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('搜索结果滚动FPS应大于30', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
await page.fill('.search-input', '春节');
await page.click('.search-btn');
await page.waitForSelector('.search-results');
const metrics = await measureAnimationPerformance(page, async () => {
await page.evaluate(() => {
const container = document.querySelector('.search-results');
if (container) {
container.scrollTop = 500;
}
});
}, 500);
console.log('搜索结果滚动动画指标:', metrics);
expect(metrics.fps).toBeGreaterThan(ANIMATION_THRESHOLDS.fps);
});
test('滚动掉帧数应小于5', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const metrics = await measureAnimationPerformance(page, async () => {
await page.evaluate(() => {
const container = document.querySelector('.calendar-container');
if (container) {
container.scrollTop = 500;
}
});
}, 500);
console.log('滚动掉帧数:', metrics.droppedFrames);
expect(metrics.droppedFrames).toBeLessThan(5);
});
test('滚动帧时间应小于33.33ms', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const metrics = await measureAnimationPerformance(page, async () => {
await page.evaluate(() => {
const container = document.querySelector('.calendar-container');
if (container) {
container.scrollTop = 500;
}
});
}, 500);
console.log('滚动帧时间:', metrics.frameTime);
expect(metrics.frameTime).toBeLessThan(ANIMATION_THRESHOLDS.frameTime);
});
});
@@ -0,0 +1,269 @@
export interface PerformanceThresholds {
pageLoadTime: number;
firstContentfulPaint: number;
largestContentfulPaint: number;
timeToInteractive: number;
cumulativeLayoutShift: number;
firstInputDelay: number;
fps: number;
frameTime: number;
animationDuration: number;
}
export const PERFORMANCE_THRESHOLDS: PerformanceThresholds = {
pageLoadTime: 2000,
firstContentfulPaint: 1000,
largestContentfulPaint: 1500,
timeToInteractive: 2000,
cumulativeLayoutShift: 0.1,
firstInputDelay: 100,
fps: 30,
frameTime: 33.33,
animationDuration: 500,
};
export interface PerformanceMetrics {
pageLoadTime: number;
firstContentfulPaint: number;
largestContentfulPaint: number;
timeToInteractive: number;
cumulativeLayoutShift: number;
firstInputDelay: number;
fps: number;
frameTime: number;
droppedFrames: number;
totalFrames: number;
animationDuration: number;
}
export interface PerformanceReport {
timestamp: string;
url: string;
platform: string;
metrics: PerformanceMetrics;
thresholds: PerformanceThresholds;
passed: boolean;
issues: PerformanceIssue[];
}
export interface PerformanceIssue {
type: 'page-load' | 'animation' | 'rendering';
metric: string;
actual: number;
expected: number;
severity: 'critical' | 'warning' | 'info';
message: string;
}
export class PerformanceMonitor {
private metrics: PerformanceMetrics;
private thresholds: PerformanceThresholds;
private issues: PerformanceIssue[];
constructor(thresholds?: Partial<PerformanceThresholds>) {
this.thresholds = { ...PERFORMANCE_THRESHOLDS, ...thresholds };
this.metrics = this.initializeMetrics();
this.issues = [];
}
private initializeMetrics(): PerformanceMetrics {
return {
pageLoadTime: 0,
firstContentfulPaint: 0,
largestContentfulPaint: 0,
timeToInteractive: 0,
cumulativeLayoutShift: 0,
firstInputDelay: 0,
fps: 0,
frameTime: 0,
droppedFrames: 0,
totalFrames: 0,
animationDuration: 0,
};
}
public setMetric(key: keyof PerformanceMetrics, value: number): void {
this.metrics[key] = value;
}
public getMetric(key: keyof PerformanceMetrics): number {
return this.metrics[key];
}
public getMetrics(): PerformanceMetrics {
return { ...this.metrics };
}
public checkThresholds(): void {
this.issues = [];
if (this.metrics.pageLoadTime > this.thresholds.pageLoadTime) {
this.issues.push({
type: 'page-load',
metric: 'pageLoadTime',
actual: this.metrics.pageLoadTime,
expected: this.thresholds.pageLoadTime,
severity: 'critical',
message: `页面加载时间 ${this.metrics.pageLoadTime}ms 超过阈值 ${this.thresholds.pageLoadTime}ms`,
});
}
if (this.metrics.firstContentfulPaint > this.thresholds.firstContentfulPaint) {
this.issues.push({
type: 'page-load',
metric: 'firstContentfulPaint',
actual: this.metrics.firstContentfulPaint,
expected: this.thresholds.firstContentfulPaint,
severity: 'warning',
message: `首次内容绘制 ${this.metrics.firstContentfulPaint}ms 超过阈值 ${this.thresholds.firstContentfulPaint}ms`,
});
}
if (this.metrics.largestContentfulPaint > this.thresholds.largestContentfulPaint) {
this.issues.push({
type: 'page-load',
metric: 'largestContentfulPaint',
actual: this.metrics.largestContentfulPaint,
expected: this.thresholds.largestContentfulPaint,
severity: 'warning',
message: `最大内容绘制 ${this.metrics.largestContentfulPaint}ms 超过阈值 ${this.thresholds.largestContentfulPaint}ms`,
});
}
if (this.metrics.timeToInteractive > this.thresholds.timeToInteractive) {
this.issues.push({
type: 'page-load',
metric: 'timeToInteractive',
actual: this.metrics.timeToInteractive,
expected: this.thresholds.timeToInteractive,
severity: 'warning',
message: `可交互时间 ${this.metrics.timeToInteractive}ms 超过阈值 ${this.thresholds.timeToInteractive}ms`,
});
}
if (this.metrics.cumulativeLayoutShift > this.thresholds.cumulativeLayoutShift) {
this.issues.push({
type: 'rendering',
metric: 'cumulativeLayoutShift',
actual: this.metrics.cumulativeLayoutShift,
expected: this.thresholds.cumulativeLayoutShift,
severity: 'warning',
message: `累积布局偏移 ${this.metrics.cumulativeLayoutShift} 超过阈值 ${this.thresholds.cumulativeLayoutShift}`,
});
}
if (this.metrics.firstInputDelay > this.thresholds.firstInputDelay) {
this.issues.push({
type: 'page-load',
metric: 'firstInputDelay',
actual: this.metrics.firstInputDelay,
expected: this.thresholds.firstInputDelay,
severity: 'warning',
message: `首次输入延迟 ${this.metrics.firstInputDelay}ms 超过阈值 ${this.thresholds.firstInputDelay}ms`,
});
}
if (this.metrics.fps < this.thresholds.fps) {
this.issues.push({
type: 'animation',
metric: 'fps',
actual: this.metrics.fps,
expected: this.thresholds.fps,
severity: 'critical',
message: `动画帧率 ${this.metrics.fps}fps 低于阈值 ${this.thresholds.fps}fps`,
});
}
if (this.metrics.frameTime > this.thresholds.frameTime) {
this.issues.push({
type: 'animation',
metric: 'frameTime',
actual: this.metrics.frameTime,
expected: this.thresholds.frameTime,
severity: 'warning',
message: `帧时间 ${this.metrics.frameTime}ms 超过阈值 ${this.thresholds.frameTime}ms`,
});
}
}
public getIssues(): PerformanceIssue[] {
return [...this.issues];
}
public hasCriticalIssues(): boolean {
return this.issues.some(issue => issue.severity === 'critical');
}
public hasWarnings(): boolean {
return this.issues.some(issue => issue.severity === 'warning');
}
public generateReport(url: string, platform: string): PerformanceReport {
this.checkThresholds();
return {
timestamp: new Date().toISOString(),
url,
platform,
metrics: this.getMetrics(),
thresholds: this.thresholds,
passed: !this.hasCriticalIssues(),
issues: this.getIssues(),
};
}
public reset(): void {
this.metrics = this.initializeMetrics();
this.issues = [];
}
}
export const createPerformanceMonitor = (thresholds?: Partial<PerformanceThresholds>): PerformanceMonitor => {
return new PerformanceMonitor(thresholds);
};
export const generatePerformanceReportSummary = (reports: PerformanceReport[]): string => {
const totalReports = reports.length;
const passedReports = reports.filter(report => report.passed).length;
const failedReports = totalReports - passedReports;
const totalIssues = reports.reduce((sum, report) => sum + report.issues.length, 0);
const criticalIssues = reports.reduce((sum, report) => sum + report.issues.filter(issue => issue.severity === 'critical').length, 0);
const warningIssues = reports.reduce((sum, report) => sum + report.issues.filter(issue => issue.severity === 'warning').length, 0);
const avgPageLoadTime = reports.reduce((sum, report) => sum + report.metrics.pageLoadTime, 0) / totalReports;
const avgFPS = reports.reduce((sum, report) => sum + report.metrics.fps, 0) / totalReports;
return `
性能测试报告摘要
================
测试时间: ${new Date().toISOString()}
测试数量: ${totalReports}
通过数量: ${passedReports}
失败数量: ${failedReports}
通过率: ${((passedReports / totalReports) * 100).toFixed(2)}%
问题统计
--------
总问题数: ${totalIssues}
严重问题: ${criticalIssues}
警告问题: ${warningIssues}
性能指标
--------
平均页面加载时间: ${avgPageLoadTime.toFixed(2)}ms
平均动画帧率: ${avgFPS.toFixed(2)}fps
详细信息
--------
${reports.map((report, index) => `
报告 ${index + 1}: ${report.url}
- 平台: ${report.platform}
- 状态: ${report.passed ? '通过' : '失败'}
- 页面加载时间: ${report.metrics.pageLoadTime}ms
- 动画帧率: ${report.metrics.fps}fps
- 问题数: ${report.issues.length}
${report.issues.length > 0 ? ` 问题详情:\n${report.issues.map(issue => ` - [${issue.severity.toUpperCase()}] ${issue.message}`).join('\n')}` : ''}
`).join('\n')}
`;
};
@@ -0,0 +1,502 @@
import { test, expect, Page } from '@playwright/test';
const PERFORMANCE_THRESHOLDS = {
pageLoadTime: 2000,
firstContentfulPaint: 1000,
largestContentfulPaint: 1500,
timeToInteractive: 2000,
cumulativeLayoutShift: 0.1,
firstInputDelay: 100,
};
interface PerformanceMetrics {
pageLoadTime: number;
firstContentfulPaint: number;
largestContentfulPaint: number;
timeToInteractive: number;
cumulativeLayoutShift: number;
firstInputDelay: number;
}
async function measurePagePerformance(page: Page, url: string): Promise<PerformanceMetrics> {
const startTime = Date.now();
await page.goto(url, { waitUntil: 'networkidle' });
const pageLoadTime = Date.now() - startTime;
const metrics = await page.evaluate(() => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
const paint = performance.getEntriesByType('paint');
const fcp = paint.find(entry => entry.name === 'first-contentful-paint')?.startTime || 0;
return {
pageLoadTime: 0,
firstContentfulPaint: fcp,
largestContentfulPaint: 0,
timeToInteractive: navigation.domInteractive - navigation.startTime,
cumulativeLayoutShift: 0,
firstInputDelay: 0,
};
});
metrics.pageLoadTime = pageLoadTime;
const lcp = await page.evaluate(() => {
return new Promise<number>((resolve) => {
if (!('PerformanceObserver' in window)) {
resolve(0);
return;
}
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
resolve(lastEntry.startTime);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
setTimeout(() => {
observer.disconnect();
resolve(0);
}, 5000);
});
});
metrics.largestContentfulPaint = lcp;
const cls = await page.evaluate(() => {
return new Promise<number>((resolve) => {
if (!('PerformanceObserver' in window)) {
resolve(0);
return;
}
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += (entry as any).value;
}
}
});
observer.observe({ entryTypes: ['layout-shift'] });
setTimeout(() => {
observer.disconnect();
resolve(clsValue);
}, 5000);
});
});
metrics.cumulativeLayoutShift = cls;
const fid = await page.evaluate(() => {
return new Promise<number>((resolve) => {
if (!('PerformanceObserver' in window)) {
resolve(0);
return;
}
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
resolve((entry as any).processingStart - entry.startTime);
}
});
observer.observe({ entryTypes: ['first-input'] });
setTimeout(() => {
observer.disconnect();
resolve(0);
}, 5000);
});
});
metrics.firstInputDelay = fid;
return metrics;
}
test.describe('日历页面加载性能测试', () => {
test('日历首页加载时间应小于2秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
console.log('日历首页性能指标:', metrics);
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('日历首页首次内容绘制应小于1秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
console.log('日历首页FCP:', metrics.firstContentfulPaint);
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
});
test('日历首页最大内容绘制应小于1.5秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
console.log('日历首页LCP:', metrics.largestContentfulPaint);
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
});
test('日历首页可交互时间应小于2秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
console.log('日历首页TTI:', metrics.timeToInteractive);
expect(metrics.timeToInteractive).toBeLessThan(PERFORMANCE_THRESHOLDS.timeToInteractive);
});
test('日历首页累积布局偏移应小于0.1', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/calendar/index');
console.log('日历首页CLS:', metrics.cumulativeLayoutShift);
expect(metrics.cumulativeLayoutShift).toBeLessThan(PERFORMANCE_THRESHOLDS.cumulativeLayoutShift);
});
test('日历详情页加载时间应小于2秒', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.click('.calendar-day[data-date="2026-02-11"]');
const startTime = Date.now();
await page.waitForURL('**/calendar/detail**');
const loadTime = Date.now() - startTime;
console.log('日历详情页加载时间:', loadTime);
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('日历月切换加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const startTime = Date.now();
await page.click('.calendar-next-month');
await page.waitForSelector('.calendar-container');
const loadTime = Date.now() - startTime;
console.log('日历月切换加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('日历年切换加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const startTime = Date.now();
await page.click('.calendar-year-selector');
await page.click('.calendar-year-option[data-year="2027"]');
await page.waitForSelector('.calendar-container');
const loadTime = Date.now() - startTime;
console.log('日历年切换加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('日历今日跳转加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
const startTime = Date.now();
await page.click('.calendar-today-btn');
await page.waitForSelector('.calendar-container');
const loadTime = Date.now() - startTime;
console.log('日历今日跳转加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('日历搜索结果加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/calendar/index');
await page.waitForSelector('.calendar-container');
await page.fill('.calendar-search-input', '2026-02-14');
const startTime = Date.now();
await page.click('.calendar-search-btn');
await page.waitForSelector('.calendar-day[data-date="2026-02-14"]');
const loadTime = Date.now() - startTime;
console.log('日历搜索结果加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
});
test.describe('黄历页面加载性能测试', () => {
test('黄历首页加载时间应小于2秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
console.log('黄历首页性能指标:', metrics);
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('黄历首页首次内容绘制应小于1秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
console.log('黄历首页FCP:', metrics.firstContentfulPaint);
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
});
test('黄历首页最大内容绘制应小于1.5秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/almanac/index');
console.log('黄历首页LCP:', metrics.largestContentfulPaint);
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
});
test('黄历详情页加载时间应小于2秒', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.click('.almanac-day[data-date="2026-02-11"]');
const startTime = Date.now();
await page.waitForURL('**/almanac/detail**');
const loadTime = Date.now() - startTime;
console.log('黄历详情页加载时间:', loadTime);
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('黄历月切换加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('.almanac-container');
const startTime = Date.now();
await page.click('.almanac-next-month');
await page.waitForSelector('.almanac-container');
const loadTime = Date.now() - startTime;
console.log('黄历月切换加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('黄历宜忌筛选加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('.almanac-container');
const startTime = Date.now();
await page.click('.almanac-filter-btn');
await page.click('.filter-checkbox[data-value="嫁娶"]');
await page.click('.filter-confirm-btn');
await page.waitForSelector('.almanac-container');
const loadTime = Date.now() - startTime;
console.log('黄历宜忌筛选加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('黄历搜索结果加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/almanac/index');
await page.waitForSelector('.almanac-container');
await page.fill('.almanac-search-input', '春节');
const startTime = Date.now();
await page.click('.almanac-search-btn');
await page.waitForSelector('.almanac-search-result');
const loadTime = Date.now() - startTime;
console.log('黄历搜索结果加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
});
test.describe('用户页面加载性能测试', () => {
test('用户首页加载时间应小于2秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/user/index');
console.log('用户首页性能指标:', metrics);
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('用户首页首次内容绘制应小于1秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/user/index');
console.log('用户首页FCP:', metrics.firstContentfulPaint);
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
});
test('用户首页最大内容绘制应小于1.5秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/user/index');
console.log('用户首页LCP:', metrics.largestContentfulPaint);
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
});
test('用户设置页加载时间应小于2秒', async ({ page }) => {
await page.goto('/pages/user/index');
await page.click('.user-settings-btn');
const startTime = Date.now();
await page.waitForURL('**/user/settings**');
const loadTime = Date.now() - startTime;
console.log('用户设置页加载时间:', loadTime);
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('用户收藏页加载时间应小于2秒', async ({ page }) => {
await page.goto('/pages/user/index');
await page.click('.user-favorites-btn');
const startTime = Date.now();
await page.waitForURL('**/user/favorites**');
const loadTime = Date.now() - startTime;
console.log('用户收藏页加载时间:', loadTime);
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('用户历史页加载时间应小于2秒', async ({ page }) => {
await page.goto('/pages/user/index');
await page.click('.user-history-btn');
const startTime = Date.now();
await page.waitForURL('**/user/history**');
const loadTime = Date.now() - startTime;
console.log('用户历史页加载时间:', loadTime);
expect(loadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('用户主题切换加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/user/index');
await page.waitForSelector('.user-container');
const startTime = Date.now();
await page.click('.theme-switch-btn');
await page.waitForSelector('.user-container');
const loadTime = Date.now() - startTime;
console.log('用户主题切换加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
});
test.describe('搜索页面加载性能测试', () => {
test('搜索首页加载时间应小于2秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/search/index');
console.log('搜索首页性能指标:', metrics);
expect(metrics.pageLoadTime).toBeLessThan(PERFORMANCE_THRESHOLDS.pageLoadTime);
});
test('搜索首页首次内容绘制应小于1秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/search/index');
console.log('搜索首页FCP:', metrics.firstContentfulPaint);
expect(metrics.firstContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.firstContentfulPaint);
});
test('搜索首页最大内容绘制应小于1.5秒', async ({ page }) => {
const metrics = await measurePagePerformance(page, '/pages/search/index');
console.log('搜索首页LCP:', metrics.largestContentfulPaint);
expect(metrics.largestContentfulPaint).toBeLessThan(PERFORMANCE_THRESHOLDS.largestContentfulPaint);
});
test('搜索结果加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
await page.fill('.search-input', '春节');
const startTime = Date.now();
await page.click('.search-btn');
await page.waitForSelector('.search-results');
const loadTime = Date.now() - startTime;
console.log('搜索结果加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('搜索建议加载时间应小于500毫秒', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
await page.fill('.search-input', '春');
const startTime = Date.now();
await page.waitForSelector('.search-suggestions');
const loadTime = Date.now() - startTime;
console.log('搜索建议加载时间:', loadTime);
expect(loadTime).toBeLessThan(500);
});
test('搜索历史加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
const startTime = Date.now();
await page.click('.search-history-btn');
await page.waitForSelector('.search-history-list');
const loadTime = Date.now() - startTime;
console.log('搜索历史加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('搜索热门加载时间应小于1秒', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
const startTime = Date.now();
await page.click('.search-hot-btn');
await page.waitForSelector('.search-hot-list');
const loadTime = Date.now() - startTime;
console.log('搜索热门加载时间:', loadTime);
expect(loadTime).toBeLessThan(1000);
});
test('搜索分类切换加载时间应小于500毫秒', async ({ page }) => {
await page.goto('/pages/search/index');
await page.waitForSelector('.search-input');
await page.fill('.search-input', '春节');
await page.click('.search-btn');
await page.waitForSelector('.search-results');
const startTime = Date.now();
await page.click('.search-category[data-category="almanac"]');
await page.waitForSelector('.search-results');
const loadTime = Date.now() - startTime;
console.log('搜索分类切换加载时间:', loadTime);
expect(loadTime).toBeLessThan(500);
});
});
@@ -0,0 +1,175 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const RESULTS_DIR = path.join(__dirname, '../../test-results/performance');
const REPORT_FILE = path.join(RESULTS_DIR, 'performance-report.json');
const SUMMARY_FILE = path.join(RESULTS_DIR, 'performance-summary.txt');
function ensureResultsDir() {
if (!fs.existsSync(RESULTS_DIR)) {
fs.mkdirSync(RESULTS_DIR, { recursive: true });
}
}
function runPerformanceTests() {
console.log('开始运行性能测试...');
console.log('='.repeat(50));
ensureResultsDir();
try {
const startTime = Date.now();
execSync('npx playwright test e2e/performance/page-load.spec.ts --reporter=json --reporter-file=test-results/performance/page-load.json', {
stdio: 'inherit',
cwd: path.join(__dirname, '../..'),
});
execSync('npx playwright test e2e/performance/animation.spec.ts --reporter=json --reporter-file=test-results/performance/animation.json', {
stdio: 'inherit',
cwd: path.join(__dirname, '../..'),
});
const endTime = Date.now();
const duration = ((endTime - startTime) / 1000).toFixed(2);
console.log('='.repeat(50));
console.log(`性能测试完成,耗时: ${duration}`);
console.log('='.repeat(50));
generateSummary();
return { success: true, duration };
} catch (error) {
console.error('性能测试失败:', error.message);
return { success: false, error: error.message };
}
}
function generateSummary() {
console.log('生成性能测试摘要...');
const pageLoadResults = readTestResults('page-load.json');
const animationResults = readTestResults('animation.json');
const summary = generatePerformanceSummary(pageLoadResults, animationResults);
fs.writeFileSync(SUMMARY_FILE, summary, 'utf-8');
console.log('性能测试摘要已生成:', SUMMARY_FILE);
}
function readTestResults(filename) {
const filePath = path.join(RESULTS_DIR, filename);
if (!fs.existsSync(filePath)) {
return { suites: [], specs: [], tests: [] };
}
const content = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(content);
}
function generatePerformanceSummary(pageLoadResults, animationResults) {
const pageLoadTests = pageLoadResults.tests || [];
const animationTests = animationResults.tests || [];
const totalTests = pageLoadTests.length + animationTests.length;
const passedTests = [...pageLoadTests, ...animationTests].filter(test => test.status === 'passed').length;
const failedTests = totalTests - passedTests;
const summary = `
性能测试报告摘要
================
测试时间: ${new Date().toISOString()}
测试数量: ${totalTests}
通过数量: ${passedTests}
失败数量: ${failedTests}
通过率: ${((passedTests / totalTests) * 100).toFixed(2)}%
页面加载性能测试
----------------
测试数量: ${pageLoadTests.length}
通过数量: ${pageLoadTests.filter(test => test.status === 'passed').length}
失败数量: ${pageLoadTests.filter(test => test.status === 'failed').length}
动画性能测试
------------
测试数量: ${animationTests.length}
通过数量: ${animationTests.filter(test => test.status === 'passed').length}
失败数量: ${animationTests.filter(test => test.status === 'failed').length}
失败测试详情
------------
${[...pageLoadTests, ...animationTests]
.filter(test => test.status === 'failed')
.map((test, index) => `
${index + 1}. ${test.title}
- 文件: ${test.location.file}
- 行号: ${test.location.line}
- 错误: ${test.error?.message || '未知错误'}
`).join('\n')}
性能指标
--------
页面加载性能阈值:
- 页面加载时间: < 2000ms
- 首次内容绘制: < 1000ms
- 最大内容绘制: < 1500ms
- 可交互时间: < 2000ms
- 累积布局偏移: < 0.1
- 首次输入延迟: < 100ms
动画性能阈值:
- 动画帧率: > 30fps
- 帧时间: < 33.33ms
- 动画持续时间: < 500ms
建议
----
${failedTests > 0 ? `
1. 优先修复失败的测试用例
2. 检查性能指标是否超过阈值
3. 优化页面加载性能
4. 优化动画性能
` : `
1. 持续监控性能指标
2. 定期运行性能测试
3. 记录性能基线
4. 及时发现性能退化
`}
`;
return summary;
}
function main() {
const args = process.argv.slice(2);
const command = args[0] || 'run';
switch (command) {
case 'run':
const result = runPerformanceTests();
process.exit(result.success ? 0 : 1);
break;
case 'summary':
generateSummary();
process.exit(0);
break;
default:
console.log('用法: node run-tests.js [run|summary]');
console.log(' run - 运行性能测试');
console.log(' summary - 生成性能测试摘要');
process.exit(1);
}
}
if (require.main === module) {
main();
}
module.exports = {
runPerformanceTests,
generateSummary,
};
@@ -0,0 +1,352 @@
import { test, expect } from '@playwright/test';
test.describe('Web平台兼容性测试', () => {
let baseUrl: string;
test.beforeAll(async () => {
baseUrl = 'http://localhost:8081';
});
test.beforeEach(async ({ page }) => {
await page.goto(baseUrl);
await page.waitForLoadState('networkidle');
});
test.describe('TC-001: 页面加载测试', () => {
test('应该能够加载页面', async ({ page }) => {
await page.waitForLoadState('networkidle');
const pageTitle = await page.title();
expect(pageTitle).toBeTruthy();
const bodyContent = await page.evaluate(() => document.body.innerText);
expect(bodyContent.length).toBeGreaterThan(0);
});
test('页面应该正常显示', async ({ page }) => {
await page.waitForLoadState('networkidle');
const isPageVisible = await page.evaluate(() => {
return document.visibilityState === 'visible';
});
expect(isPageVisible).toBe(true);
});
test('页面加载时不应该有错误提示', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1500);
const criticalErrors = errors.filter(e =>
e.includes('TypeError') ||
e.includes('ReferenceError') ||
e.includes('SyntaxError')
);
expect(criticalErrors.length).toBeLessThan(3);
});
});
test.describe('TC-002: CSS变量测试', () => {
test('CSS变量应该正确设置', async ({ page }) => {
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const cssVars = await page.evaluate(() => {
const root = document.documentElement;
const style = getComputedStyle(root);
const properties = Array.from(style).filter(prop =>
prop.includes('--') || prop.includes('theme')
);
return properties.length;
});
expect(cssVars).toBeGreaterThan(0);
});
test('CSS变量应该能够被继承和覆盖', async ({ page }) => {
await page.waitForLoadState('networkidle');
await page.waitForTimeout(1000);
const rootHasVars = await page.evaluate(() => {
const root = document.documentElement;
const style = getComputedStyle(root);
const properties = Array.from(style).filter(prop =>
prop.includes('--') || prop.includes('theme')
);
return properties.length > 0;
});
const bodyHasVars = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
const properties = Array.from(style).filter(prop =>
prop.includes('--') || prop.includes('theme')
);
return properties.length > 0;
});
expect(rootHasVars).toBe(true);
expect(bodyHasVars).toBe(true);
});
});
test.describe('TC-003: 字体系统测试', () => {
test('字体应该正确加载', async ({ page }) => {
await page.waitForLoadState('networkidle');
const fontsLoaded = await page.evaluate(() => {
return document.fonts.ready.then(() => {
return document.fonts.size > 0;
});
});
expect(fontsLoaded).toBe(true);
});
test('字体大小应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const fontSize = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return style.fontSize;
});
expect(fontSize).toBeTruthy();
expect(fontSize).not.toBe('0px');
});
test('字体粗细应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const fontWeight = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return style.fontWeight;
});
expect(fontWeight).toBeTruthy();
});
});
test.describe('TC-004: SVG纹样测试', () => {
test('SVG纹样应该正确显示', async ({ page }) => {
await page.waitForLoadState('networkidle');
const svgElements = await page.evaluate(() => {
const svgs = document.querySelectorAll('svg');
return svgs.length;
});
expect(svgElements).toBeGreaterThanOrEqual(0);
});
test('SVG纹样应该能够缩放', async ({ page }) => {
await page.waitForLoadState('networkidle');
const svgElements = await page.evaluate(() => {
const svgs = document.querySelectorAll('svg');
return Array.from(svgs).map(svg => {
const style = getComputedStyle(svg);
return {
width: style.width,
height: style.height
};
});
});
expect(svgElements.length).toBeGreaterThanOrEqual(0);
});
test('SVG纹样应该能够应用颜色', async ({ page }) => {
await page.waitForLoadState('networkidle');
const svgElements = await page.evaluate(() => {
const svgs = document.querySelectorAll('svg');
return Array.from(svgs).map(svg => {
const style = getComputedStyle(svg);
return style.fill;
});
});
expect(svgElements.length).toBeGreaterThanOrEqual(0);
});
});
test.describe('TC-005: 动画效果测试', () => {
test('动画应该正常播放', async ({ page }) => {
await page.waitForLoadState('networkidle');
const animatedElements = await page.evaluate(() => {
const elements = document.querySelectorAll('*');
return Array.from(elements).filter(el => {
const style = getComputedStyle(el);
return style.animationName !== 'none' ||
style.transitionProperty !== 'none';
}).length;
});
expect(animatedElements).toBeGreaterThanOrEqual(0);
});
test('动画应该流畅无卡顿', async ({ page }) => {
await page.waitForLoadState('networkidle');
const startTime = Date.now();
await page.evaluate(() => {
return new Promise(resolve => setTimeout(resolve, 1000));
});
const endTime = Date.now();
expect(endTime - startTime).toBeLessThan(2000);
});
test('动画性能应该良好', async ({ page }) => {
await page.waitForLoadState('networkidle');
const performanceMetrics = await page.evaluate(() => {
if (window.performance && window.performance.memory) {
return {
usedJSHeapSize: window.performance.memory.usedJSHeapSize,
totalJSHeapSize: window.performance.memory.totalJSHeapSize
};
}
return null;
});
expect(performanceMetrics).toBeTruthy();
});
});
test.describe('TC-006: 组件样式测试', () => {
test('日历组件样式应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const calendarElements = await page.evaluate(() => {
const elements = document.querySelectorAll('[class*="calendar"], [class*="Calendar"]');
return elements.length;
});
expect(calendarElements).toBeGreaterThanOrEqual(0);
});
test('导航组件样式应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const navElements = await page.evaluate(() => {
const elements = document.querySelectorAll('[class*="nav"], [class*="Nav"]');
return elements.length;
});
expect(navElements).toBeGreaterThanOrEqual(0);
});
test('卡片组件样式应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const cardElements = await page.evaluate(() => {
const elements = document.querySelectorAll('[class*="card"], [class*="Card"]');
return elements.length;
});
expect(cardElements).toBeGreaterThanOrEqual(0);
});
});
test.describe('TC-007: 页面样式测试', () => {
test('日历页面样式应该正确应用', async ({ page }) => {
await page.waitForLoadState('networkidle');
const pageContent = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return {
backgroundColor: style.backgroundColor,
color: style.color
};
});
expect(pageContent.backgroundColor).toBeTruthy();
expect(pageContent.color).toBeTruthy();
});
test('页面布局应该正常', async ({ page }) => {
await page.waitForLoadState('networkidle');
const pageLayout = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return {
width: style.width,
height: style.height,
display: style.display
};
});
expect(pageLayout.width).toBeTruthy();
expect(pageLayout.height).toBeTruthy();
});
test('页面响应式应该正常', async ({ page }) => {
await page.waitForLoadState('networkidle');
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(500);
const mobileLayout = await page.evaluate(() => {
const body = document.body;
const style = getComputedStyle(body);
return {
width: style.width,
display: style.display
};
});
expect(mobileLayout.width).toBeTruthy();
});
});
test.describe('TC-008: 功能测试', () => {
test('日历功能应该正常', async ({ page }) => {
await page.waitForLoadState('networkidle');
const calendarExists = await page.evaluate(() => {
const elements = document.querySelectorAll('[class*="calendar"], [class*="Calendar"]');
return elements.length > 0;
});
expect(calendarExists).toBe(true);
});
test('导航功能应该正常', async ({ page }) => {
await page.waitForLoadState('networkidle');
const navExists = await page.evaluate(() => {
const elements = document.querySelectorAll('[class*="nav"], [class*="Nav"]');
return elements.length > 0;
});
expect(navExists).toBe(true);
});
test('用户界面应该正常', async ({ page }) => {
await page.waitForLoadState('networkidle');
const userInterfaceExists = await page.evaluate(() => {
const body = document.body;
return body.innerText.length > 0;
});
expect(userInterfaceExists).toBe(true);
});
});
});