feat(admin): 添加用户管理相关文件
添加用户管理视图、API和状态管理文件
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user