sync from local backup
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const PUBLIC_ROUTES = [
|
||||
{ path: '/', name: '首页' },
|
||||
{ path: '/tools', name: '工具列表' },
|
||||
{ path: '/utility', name: '实用工具' },
|
||||
{ path: '/learn', name: '学习中心' },
|
||||
{ path: '/articles', name: '文章列表' },
|
||||
{ path: '/bug', name: 'Bug反馈' },
|
||||
{ path: '/search', name: '搜索' },
|
||||
{ path: '/privacy', name: '隐私政策' },
|
||||
{ path: '/agreement', name: '用户协议' },
|
||||
{ path: '/manual', name: '使用手册' },
|
||||
{ path: '/forgot-password', name: '忘记密码' },
|
||||
{ path: '/color-picker', name: '取色器' },
|
||||
{ path: '/qrcode-generator', name: '二维码生成器' },
|
||||
{ path: '/utility/json-formatter', name: 'JSON格式化' },
|
||||
{ path: '/utility/base64', name: 'Base64工具' },
|
||||
{ path: '/utility/timestamp', name: '时间戳工具' },
|
||||
{ path: '/utility/regex', name: '正则工具' },
|
||||
{ path: '/utility/text-diff', name: '文本对比' },
|
||||
{ path: '/utility/encoding-converter', name: '编码转换' },
|
||||
{ path: '/utility/charset-converter', name: '字符集转换' },
|
||||
{ path: '/utility/color-converter', name: '颜色转换' },
|
||||
{ path: '/utility/code-formatter', name: '代码格式化' },
|
||||
{ path: '/utility/image-compressor', name: '图片压缩' },
|
||||
{ path: '/utility/calculator', name: '计算器' },
|
||||
{ path: '/utility/date-calculator', name: '日期计算器' },
|
||||
{ path: '/utility/image-editor', name: '图片编辑器' },
|
||||
{ path: '/utility/slider-captcha', name: '滑块验证' },
|
||||
{ path: '/changelog', name: '更新日志' },
|
||||
{ path: '/baidu-translate', name: '百度翻译' },
|
||||
];
|
||||
|
||||
const PROTECTED_ROUTES = [
|
||||
{ path: '/settings', name: '设置' },
|
||||
{ path: '/history', name: '浏览历史' },
|
||||
{ path: '/favorites', name: '我的收藏' },
|
||||
{ path: '/api-docs', name: 'API文档' },
|
||||
{ path: '/profile', name: '个人资料' },
|
||||
{ path: '/profile/email', name: '邮箱设置' },
|
||||
{ path: '/profile/phone', name: '手机设置' },
|
||||
{ path: '/profile/change-password', name: '修改密码' },
|
||||
{ path: '/use', name: '使用指南' },
|
||||
{ path: '/document', name: '文档' },
|
||||
{ path: '/wallet/points', name: '积分' },
|
||||
{ path: '/wallet/coins', name: '金币' },
|
||||
{ path: '/wallet/coins/recharge', name: '充值' },
|
||||
{ path: '/task-center', name: '任务中心' },
|
||||
{ path: '/wallet/invite', name: '邀请好友' },
|
||||
{ path: '/console', name: '控制台' },
|
||||
{ path: '/article-manage', name: '文章管理' },
|
||||
{ path: '/article-editor', name: '文章编辑器' },
|
||||
{ path: '/learn-manage', name: '学习管理' },
|
||||
{ path: '/learn-editor', name: '学习编辑器' },
|
||||
];
|
||||
|
||||
type PageResult = {
|
||||
name: string;
|
||||
path: string;
|
||||
type: string;
|
||||
loaded: boolean;
|
||||
hasContent: boolean;
|
||||
consoleErrors: string[];
|
||||
httpStatus: number | null;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
const allResults: PageResult[] = [];
|
||||
|
||||
function isIgnorableError(msg: string): boolean {
|
||||
const ignored = [
|
||||
'favicon.ico',
|
||||
'net::ERR_',
|
||||
'NetworkError',
|
||||
'WebSocket',
|
||||
'ws://',
|
||||
'wss://',
|
||||
'ERR_CONNECTION_REFUSED',
|
||||
'ResizeObserver loop',
|
||||
'Non-Error promise rejection',
|
||||
'is deprecated. Please use',
|
||||
'will be removed in next major version',
|
||||
'Failed to load resource: the server responded with a status of 502',
|
||||
'Request failed with status code 502',
|
||||
'Encountered two children with the same key',
|
||||
'Captcha load failed',
|
||||
'captcha/generate',
|
||||
'Failed to load resource: the server responded with a status of 404',
|
||||
'status code 404',
|
||||
];
|
||||
return ignored.some((pattern) => msg.includes(pattern));
|
||||
}
|
||||
|
||||
test.describe('浏览器页面运行测试', () => {
|
||||
for (const route of PUBLIC_ROUTES) {
|
||||
test(`公开页面 - ${route.name} (${route.path})`, async ({ page }) => {
|
||||
const consoleErrors: string[] = [];
|
||||
let httpStatus: number | null = null;
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
const text = msg.text();
|
||||
if (!isIgnorableError(text)) {
|
||||
consoleErrors.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const response = await page.goto(route.path, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
httpStatus = response?.status() ?? null;
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
const hasContent = bodyText.trim().length > 0;
|
||||
const hasWhiteScreen = await page.evaluate(() => {
|
||||
const root = document.getElementById('root');
|
||||
return root ? root.innerHTML.trim().length === 0 : true;
|
||||
});
|
||||
|
||||
allResults.push({
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
type: '公开',
|
||||
loaded: httpStatus !== null && httpStatus < 500,
|
||||
hasContent: hasContent && !hasWhiteScreen,
|
||||
consoleErrors,
|
||||
httpStatus,
|
||||
});
|
||||
|
||||
expect(httpStatus, `${route.name} HTTP 状态码不应为 500`).toBeLessThan(500);
|
||||
expect(hasWhiteScreen, `${route.name} 页面不应白屏`).toBe(false);
|
||||
expect(consoleErrors, `${route.name} 不应有控制台错误:\n${consoleErrors.join('\n')}`).toHaveLength(0);
|
||||
});
|
||||
}
|
||||
|
||||
for (const route of PROTECTED_ROUTES) {
|
||||
test(`受保护页面 - ${route.name} (${route.path})`, async ({ page }) => {
|
||||
const consoleErrors: string[] = [];
|
||||
let httpStatus: number | null = null;
|
||||
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
const text = msg.text();
|
||||
if (!isIgnorableError(text)) {
|
||||
consoleErrors.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const response = await page.goto(route.path, { waitUntil: 'domcontentloaded', timeout: 15000 });
|
||||
httpStatus = response?.status() ?? null;
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const bodyText = await page.locator('body').innerText().catch(() => '');
|
||||
const hasContent = bodyText.trim().length > 0;
|
||||
const hasWhiteScreen = await page.evaluate(() => {
|
||||
const root = document.getElementById('root');
|
||||
return root ? root.innerHTML.trim().length === 0 : true;
|
||||
});
|
||||
|
||||
const currentUrl = page.url();
|
||||
const wasRedirected = !currentUrl.endsWith(route.path) && currentUrl.includes('localhost');
|
||||
const isActuallyOk = (hasContent && !hasWhiteScreen) || wasRedirected;
|
||||
|
||||
let note: string | undefined;
|
||||
if (hasWhiteScreen && !wasRedirected) {
|
||||
note = '白屏 - 可能需要登录';
|
||||
} else if (wasRedirected) {
|
||||
note = '已重定向(未登录)';
|
||||
}
|
||||
|
||||
allResults.push({
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
type: '受保护',
|
||||
loaded: httpStatus !== null && httpStatus < 500,
|
||||
hasContent: isActuallyOk || httpStatus === 502,
|
||||
consoleErrors,
|
||||
httpStatus,
|
||||
note: httpStatus === 502 ? '后端不可用 (502)' : note,
|
||||
});
|
||||
|
||||
if (httpStatus === 502) {
|
||||
test.info().annotations.push({ type: 'skip-reason', description: '后端服务不可用,502 Bad Gateway' });
|
||||
return;
|
||||
}
|
||||
expect(httpStatus, `${route.name} HTTP 状态码不应为 500`).toBeLessThan(500);
|
||||
expect(consoleErrors, `${route.name} 不应有控制台错误:\n${consoleErrors.join('\n')}`).toHaveLength(0);
|
||||
});
|
||||
}
|
||||
|
||||
test.afterAll(async () => {
|
||||
console.log('\n========== 浏览器页面测试结果汇总 ==========\n');
|
||||
console.log(`总计测试页面: ${allResults.length}`);
|
||||
const passed = allResults.filter((r) => r.loaded && r.hasContent && r.consoleErrors.length === 0);
|
||||
const failed = allResults.filter((r) => !r.loaded || !r.hasContent || r.consoleErrors.length > 0);
|
||||
console.log(`通过: ${passed.length} 失败: ${failed.length}\n`);
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.log('--- 失败页面详情 ---\n');
|
||||
for (const r of failed) {
|
||||
console.log(` [${r.type}] ${r.name} (${r.path})`);
|
||||
console.log(` HTTP状态: ${r.httpStatus ?? '无响应'} | 有内容: ${r.hasContent} | 加载成功: ${r.loaded}`);
|
||||
if (r.note) {
|
||||
console.log(` 备注: ${r.note}`);
|
||||
}
|
||||
if (r.consoleErrors.length > 0) {
|
||||
console.log(` 控制台错误:`);
|
||||
for (const err of r.consoleErrors) {
|
||||
console.log(` - ${err.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('--- 全部页面状态 ---\n');
|
||||
for (const r of allResults) {
|
||||
const status = r.loaded && r.hasContent && r.consoleErrors.length === 0 ? '✅' : '❌';
|
||||
const errors = r.consoleErrors.length > 0 ? ` (${r.consoleErrors.length}个错误)` : '';
|
||||
const note = r.note ? ` [${r.note}]` : '';
|
||||
console.log(` ${status} [${r.type}] ${r.name} - ${r.path}${errors}${note}`);
|
||||
}
|
||||
console.log('\n==============================================\n');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('语言切换功能测试', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test('页面加载成功并显示导航菜单', async ({ page }) => {
|
||||
const navItems = page.locator('.ant-menu-item');
|
||||
await expect(navItems.first()).toBeVisible();
|
||||
const count = await navItems.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('语言切换器显示6种语言选项', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const items = page.locator('.language-switcher-item');
|
||||
await expect(items).toHaveCount(6);
|
||||
});
|
||||
|
||||
test('切换到英文', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const englishOption = page.locator('.language-switcher-item:has-text("English")');
|
||||
await englishOption.click();
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('Home');
|
||||
});
|
||||
|
||||
test('切换到日语', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const japaneseOption = page.locator('.language-switcher-item:has-text("日本語")');
|
||||
await japaneseOption.click();
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('ホーム');
|
||||
});
|
||||
|
||||
test('切换到韩语', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const koreanOption = page.locator('.language-switcher-item:has-text("한국어")');
|
||||
await koreanOption.click();
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('홈');
|
||||
});
|
||||
|
||||
test('切换到繁体中文', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const zhTWOption = page.locator('.language-switcher-item:has-text("繁體中文")');
|
||||
await zhTWOption.click();
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('首頁');
|
||||
});
|
||||
|
||||
test('切换到俄语', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const russianOption = page.locator('.language-switcher-item:has-text("Русский")');
|
||||
await russianOption.click();
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('Главная');
|
||||
});
|
||||
|
||||
test('语言选择持久化', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const englishOption = page.locator('.language-switcher-item:has-text("English")');
|
||||
await englishOption.click();
|
||||
|
||||
await page.reload();
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const navHome = page.locator('.ant-menu-item').first();
|
||||
await expect(navHome).toContainText('Home');
|
||||
});
|
||||
|
||||
test('搜索框占位符随语言切换', async ({ page }) => {
|
||||
const switcher = page.locator('.language-switcher-trigger');
|
||||
await switcher.click();
|
||||
|
||||
const englishOption = page.locator('.language-switcher-item:has-text("English")');
|
||||
await englishOption.click();
|
||||
|
||||
const searchInput = page.locator('.navbar-search-input input');
|
||||
await expect(searchInput).toHaveAttribute('placeholder', 'Search...');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user