458 lines
18 KiB
TypeScript
458 lines
18 KiB
TypeScript
|
|
import { test, expect, Page } from '@playwright/test';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 全流程模拟测试:采购商下单 → 分享链接 → 纺织厂确认 → 生产流程 → 分批次入库
|
|||
|
|
*
|
|||
|
|
* 测试场景:
|
|||
|
|
* 1. 采购商创建产品和生产计划
|
|||
|
|
* 2. 生成分享链接给首次合作的纺织厂
|
|||
|
|
* 3. 纺织厂点击链接并确认导入
|
|||
|
|
* 4. 纺织厂逐步确认生产流程节点
|
|||
|
|
* 5. 分批次坯布入库(3批)
|
|||
|
|
* 6. 验证数据完整性和一致性
|
|||
|
|
*
|
|||
|
|
* 前置条件:
|
|||
|
|
* - 示例布行 (11111111-1111-1111-1111-111111111101) 和示例纺织厂 (11111111-1111-1111-1111-111111111102) 已存在
|
|||
|
|
* - 两家公司之间无合作关系(首次合作场景)
|
|||
|
|
* - 坯布仓库 (22222222-2222-2222-2222-222222222202) 已存在
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// 测试用固定 ID
|
|||
|
|
const PURCHASER_COMPANY_ID = '11111111-1111-1111-1111-111111111101';
|
|||
|
|
const TEXTILE_COMPANY_ID = '11111111-1111-1111-1111-111111111102';
|
|||
|
|
const PURCHASER_USER_ID = 'a0000001-0000-0000-0000-000000000001';
|
|||
|
|
const TEXTILE_USER_ID = 'a0000002-0000-0000-0000-000000000002';
|
|||
|
|
const FABRIC_WAREHOUSE_ID = '22222222-2222-2222-2222-222222222202';
|
|||
|
|
|
|||
|
|
// 辅助函数:登录
|
|||
|
|
async function loginAs(page: Page, username: string, password: string) {
|
|||
|
|
await page.goto('/login');
|
|||
|
|
await page.fill('input[name="username"]', username);
|
|||
|
|
await page.fill('input[name="password"]', password);
|
|||
|
|
await page.click('button[type="submit"]');
|
|||
|
|
// 等待登录完成并跳转
|
|||
|
|
await page.waitForURL(/.*(purchaser|textile|washing)/, { timeout: 10000 });
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 辅助函数:等待页面加载完成
|
|||
|
|
async function waitForPageLoad(page: Page) {
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
await page.waitForTimeout(500);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test.describe('全流程模拟:采购商下单到纺织厂入库', () => {
|
|||
|
|
|
|||
|
|
test.describe('阶段一:采购商创建计划', () => {
|
|||
|
|
|
|||
|
|
test('1.1 采购商登录并进入工作台', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
|
|||
|
|
// 验证跳转到采购商工作台
|
|||
|
|
await expect(page).toHaveURL(/.*purchaser/);
|
|||
|
|
await expect(page.locator('text=首页')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 验证统计卡片显示
|
|||
|
|
const statCards = page.locator('[class*="stat"], [class*="card"]');
|
|||
|
|
expect(await statCards.count()).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('1.2 创建产品信息', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
await page.goto('/purchaser/warehouse');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证产品信息页面加载
|
|||
|
|
await expect(page.locator('text=产品信息')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 检查是否有产品列表或空状态提示
|
|||
|
|
const hasProducts = await page.locator('[class*="product"], [class*="list"]').count() > 0;
|
|||
|
|
const hasEmptyState = await page.locator('text=暂无').count() > 0;
|
|||
|
|
expect(hasProducts || hasEmptyState).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('1.3 新建纺织计划', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
await page.goto('/purchaser/plans/new');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证新建计划页面加载
|
|||
|
|
await expect(page.locator('text=新建纺织计划')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 验证表单元素存在
|
|||
|
|
const formElements = [
|
|||
|
|
'input[name="productName"], input[placeholder*="产品"]',
|
|||
|
|
'input[name="color"], input[placeholder*="颜色"]',
|
|||
|
|
'input[name="targetQuantity"], input[placeholder*="数量"]'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const selector of formElements) {
|
|||
|
|
const element = page.locator(selector).first();
|
|||
|
|
if (await element.count() > 0) {
|
|||
|
|
await expect(element).toBeVisible();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('阶段二:分享链接与首次合作', () => {
|
|||
|
|
|
|||
|
|
test('2.1 进入计划总览并分享', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
await page.goto('/purchaser/plans');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证计划总览页面加载
|
|||
|
|
await expect(page.locator('text=纺织计划总览')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 检查是否有计划列表
|
|||
|
|
const planCards = page.locator('[class*="plan"], [class*="card"]');
|
|||
|
|
const planCount = await planCards.count();
|
|||
|
|
console.log(`找到 ${planCount} 个计划卡片`);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('2.2 纺织厂通过链接导入计划', async ({ page }) => {
|
|||
|
|
// 模拟纺织厂访问导入页面
|
|||
|
|
await page.goto('/import?token=test-token');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证导入页面加载
|
|||
|
|
await expect(page).toHaveURL(/.*import/);
|
|||
|
|
|
|||
|
|
// 检查页面是否显示导入相关信息
|
|||
|
|
const hasImportContent = await page.locator('text=导入, text=计划, text=确认').count() > 0;
|
|||
|
|
expect(hasImportContent).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('阶段三:纺织厂生产流程', () => {
|
|||
|
|
|
|||
|
|
test('3.1 纺织厂登录并查看计划', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
|
|||
|
|
// 验证跳转到纺织厂工作台
|
|||
|
|
await expect(page).toHaveURL(/.*textile/);
|
|||
|
|
await expect(page.locator('text=首页')).toBeVisible({ timeout: 5000 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('3.2 查看计划总览和生产进度', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
await page.goto('/textile/plans');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证计划总览页面
|
|||
|
|
await expect(page.locator('text=接单与生产进度')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 检查是否有计划列表或空状态
|
|||
|
|
const hasPlans = await page.locator('[class*="plan"], [class*="card"]').count() > 0;
|
|||
|
|
const hasEmptyState = await page.locator('text=暂无, text=没有').count() > 0;
|
|||
|
|
expect(hasPlans || hasEmptyState).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('3.3 确认生产流程节点', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
await page.goto('/textile/plans');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证流程节点组件存在
|
|||
|
|
const processFlow = page.locator('[data-testid="process-flow"], [class*="process"], [class*="flow"]');
|
|||
|
|
const flowCount = await processFlow.count();
|
|||
|
|
console.log(`找到 ${flowCount} 个流程组件`);
|
|||
|
|
|
|||
|
|
// 如果有流程组件,验证其可见性
|
|||
|
|
if (flowCount > 0) {
|
|||
|
|
await expect(processFlow.first()).toBeVisible();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('阶段四:分批次入库', () => {
|
|||
|
|
|
|||
|
|
test('4.1 纺织厂进入坯布仓库', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
await page.goto('/textile/fabric-warehouse');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证坯布仓库页面
|
|||
|
|
await expect(page.locator('text=已生产坯布')).toBeVisible({ timeout: 5000 });
|
|||
|
|
|
|||
|
|
// 检查是否有库存列表或空状态
|
|||
|
|
const hasInventory = await page.locator('[class*="inventory"], [class*="list"], [class*="table"]').count() > 0;
|
|||
|
|
const hasEmptyState = await page.locator('text=暂无, text=空').count() > 0;
|
|||
|
|
expect(hasInventory || hasEmptyState).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('4.2 执行入库操作', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
await page.goto('/textile/fabric-warehouse');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证入库表单或按钮存在
|
|||
|
|
const inboundButton = page.locator('button:has-text("入库"), button:has-text("添加")');
|
|||
|
|
const buttonCount = await inboundButton.count();
|
|||
|
|
console.log(`找到 ${buttonCount} 个入库相关按钮`);
|
|||
|
|
|
|||
|
|
if (buttonCount > 0) {
|
|||
|
|
await expect(inboundButton.first()).toBeVisible();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('阶段五:数据验证', () => {
|
|||
|
|
|
|||
|
|
test('5.1 采购商验证计划完成状态', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
await page.goto('/purchaser/plans');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证计划列表中存在已完成计划(如果有)
|
|||
|
|
const completedBadge = page.locator('text=已完成, text=completed');
|
|||
|
|
const completedCount = await completedBadge.count();
|
|||
|
|
console.log(`找到 ${completedCount} 个已完成标记`);
|
|||
|
|
|
|||
|
|
// 页面应该正常加载
|
|||
|
|
await expect(page.locator('body')).toBeVisible();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('5.2 采购商验证成品仓库', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'purchaser', 'purchaser123');
|
|||
|
|
await page.goto('/purchaser/finished-warehouse');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证成品仓库页面加载
|
|||
|
|
await expect(page.locator('text=成品仓库')).toBeVisible({ timeout: 5000 });
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('5.3 纺织厂验证待结款', async ({ page }) => {
|
|||
|
|
await loginAs(page, 'textile', 'textile123');
|
|||
|
|
await page.goto('/textile/payments');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 验证待结款页面
|
|||
|
|
await expect(page.locator('text=待结款')).toBeVisible({ timeout: 5000 });
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('多角色协作实时同步测试', () => {
|
|||
|
|
|
|||
|
|
test('采购商和纺织厂双窗口实时同步', async ({ browser }) => {
|
|||
|
|
// 创建两个独立的浏览器上下文
|
|||
|
|
const purchaserContext = await browser.newContext();
|
|||
|
|
const textileContext = await browser.newContext();
|
|||
|
|
|
|||
|
|
const purchaserPage = await purchaserContext.newPage();
|
|||
|
|
const textilePage = await textileContext.newPage();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 采购商登录
|
|||
|
|
await loginAs(purchaserPage, 'purchaser', 'purchaser123');
|
|||
|
|
await purchaserPage.goto('/purchaser/plans');
|
|||
|
|
await waitForPageLoad(purchaserPage);
|
|||
|
|
|
|||
|
|
// 纺织厂登录
|
|||
|
|
await loginAs(textilePage, 'textile', 'textile123');
|
|||
|
|
await textilePage.goto('/textile/plans');
|
|||
|
|
await waitForPageLoad(textilePage);
|
|||
|
|
|
|||
|
|
// 验证两个页面都正常加载
|
|||
|
|
await expect(purchaserPage.locator('body')).toBeVisible();
|
|||
|
|
await expect(textilePage.locator('body')).toBeVisible();
|
|||
|
|
|
|||
|
|
console.log('双窗口实时同步测试完成');
|
|||
|
|
|
|||
|
|
} finally {
|
|||
|
|
await purchaserContext.close();
|
|||
|
|
await textileContext.close();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('数据库层面全流程验证(SQL 模拟)', () => {
|
|||
|
|
/**
|
|||
|
|
* 以下为本次模拟测试的 SQL 执行记录,可作为后续自动化测试的参考
|
|||
|
|
*
|
|||
|
|
* 完整流程 SQL 序列:
|
|||
|
|
*
|
|||
|
|
* -- 1. 清理旧的合作关系(模拟首次合作)
|
|||
|
|
* DELETE FROM company_relationships
|
|||
|
|
* WHERE purchaser_company_id = '${PURCHASER_COMPANY_ID}'
|
|||
|
|
* AND factory_company_id = '${TEXTILE_COMPANY_ID}';
|
|||
|
|
*
|
|||
|
|
* -- 2. 创建产品
|
|||
|
|
* INSERT INTO products (company_id, product_name, weight, color, fabric_code, color_code,
|
|||
|
|
* yarn_usage_per_meter, yarn_types, yarn_ratios, raw_fabric_rolls, raw_fabric_meters,
|
|||
|
|
* total_stock, production_price, warp_weight, weft_weight, total_weight)
|
|||
|
|
* VALUES ('${PURCHASER_COMPANY_ID}', '全棉斜纹布', 280, '藏青', 'QM-XW-280-01', '01',
|
|||
|
|
* 350, ARRAY['精梳棉纱','涤纶纱'], ARRAY[0.7,0.3], 0, 0, 0, 18.5, 160, 120, 280);
|
|||
|
|
*
|
|||
|
|
* -- 3. 创建生产计划
|
|||
|
|
* INSERT INTO production_plans (plan_code, product_name, color, fabric_code, color_code,
|
|||
|
|
* remark, purchaser_id, target_quantity, completed_quantity, yarn_usage_per_meter,
|
|||
|
|
* production_price, status, start_time, created_by)
|
|||
|
|
* VALUES ('TEST-2026-001', '全棉斜纹布', '藏青', 'QM-XW-280-01', '01',
|
|||
|
|
* '模拟测试订单', '${PURCHASER_COMPANY_ID}', 10000, 0, 350, 18.5, 'pending', NOW(),
|
|||
|
|
* '${PURCHASER_USER_ID}');
|
|||
|
|
*
|
|||
|
|
* -- 4. 创建纱线配比
|
|||
|
|
* INSERT INTO yarn_ratios (plan_id, yarn_name, ratio, amount_per_meter, total_amount, company_id)
|
|||
|
|
* VALUES ('{plan_id}', '精梳棉纱', 0.7, 245, 2450000, '${PURCHASER_COMPANY_ID}'),
|
|||
|
|
* ('{plan_id}', '涤纶纱', 0.3, 105, 1050000, '${PURCHASER_COMPANY_ID}');
|
|||
|
|
*
|
|||
|
|
* -- 5. 创建流程节点
|
|||
|
|
* INSERT INTO plan_process_steps (plan_id, step_type, status, company_id)
|
|||
|
|
* VALUES ('{plan_id}', 'confirm', 'pending', '${PURCHASER_COMPANY_ID}'),
|
|||
|
|
* ('{plan_id}', 'yarn_purchase', 'pending', '${PURCHASER_COMPANY_ID}'),
|
|||
|
|
* ('{plan_id}', 'dyeing', 'pending', '${PURCHASER_COMPANY_ID}'),
|
|||
|
|
* ('{plan_id}', 'machine_start', 'pending', '${PURCHASER_COMPANY_ID}'),
|
|||
|
|
* ('{plan_id}', 'fabric_warehouse', 'pending', '${PURCHASER_COMPANY_ID}');
|
|||
|
|
*
|
|||
|
|
* -- 6. 生成分享链接
|
|||
|
|
* INSERT INTO share_links (short_code, plan_id, factory_type, created_by, expires_at, status)
|
|||
|
|
* VALUES ('TEST-SIM-001', '{plan_id}', 'textile', '${PURCHASER_USER_ID}',
|
|||
|
|
* NOW() + INTERVAL '30 minutes', 'pending');
|
|||
|
|
*
|
|||
|
|
* -- 7. 纺织厂点击链接
|
|||
|
|
* UPDATE share_links SET status = 'clicked', clicked_at = NOW(), click_count = 1
|
|||
|
|
* WHERE short_code = 'TEST-SIM-001';
|
|||
|
|
*
|
|||
|
|
* -- 8. 纺织厂确认导入(创建关联 + 更新链接 + 建立合作 + 更新计划状态)
|
|||
|
|
* INSERT INTO plan_factories (plan_id, factory_id, factory_type)
|
|||
|
|
* VALUES ('{plan_id}', '${TEXTILE_COMPANY_ID}', 'textile');
|
|||
|
|
*
|
|||
|
|
* UPDATE share_links SET status = 'confirmed', responded_at = NOW(),
|
|||
|
|
* response_result = 'confirmed', used_at = NOW()
|
|||
|
|
* WHERE short_code = 'TEST-SIM-001';
|
|||
|
|
*
|
|||
|
|
* INSERT INTO company_relationships (purchaser_company_id, factory_company_id, factory_type, status)
|
|||
|
|
* VALUES ('${PURCHASER_COMPANY_ID}', '${TEXTILE_COMPANY_ID}', 'textile', 'active');
|
|||
|
|
*
|
|||
|
|
* UPDATE production_plans SET status = 'producing' WHERE id = '{plan_id}';
|
|||
|
|
*
|
|||
|
|
* -- 9. 确认所有流程节点
|
|||
|
|
* UPDATE plan_process_steps SET status = 'completed', timestamp = NOW(),
|
|||
|
|
* operator_id = '${TEXTILE_USER_ID}'
|
|||
|
|
* WHERE plan_id = '{plan_id}';
|
|||
|
|
*
|
|||
|
|
* -- 10. 分3批入库
|
|||
|
|
* INSERT INTO inventory_records (plan_id, warehouse_id, quantity, rolls, company_id, operator_id, price_per_meter)
|
|||
|
|
* VALUES ('{plan_id}', '${FABRIC_WAREHOUSE_ID}', 3000, 30, '${TEXTILE_COMPANY_ID}', '${TEXTILE_USER_ID}', 18.5),
|
|||
|
|
* ('{plan_id}', '${FABRIC_WAREHOUSE_ID}', 4000, 40, '${TEXTILE_COMPANY_ID}', '${TEXTILE_USER_ID}', 18.5),
|
|||
|
|
* ('{plan_id}', '${FABRIC_WAREHOUSE_ID}', 3000, 28, '${TEXTILE_COMPANY_ID}', '${TEXTILE_USER_ID}', 18.5);
|
|||
|
|
*
|
|||
|
|
* -- 11. 更新计划完成状态
|
|||
|
|
* UPDATE production_plans SET completed_quantity = 10000, status = 'completed'
|
|||
|
|
* WHERE id = '{plan_id}';
|
|||
|
|
*
|
|||
|
|
* -- 12. 验证查询
|
|||
|
|
* SELECT p.plan_code, p.status, p.target_quantity, p.completed_quantity,
|
|||
|
|
* (SELECT COUNT(*) FROM plan_process_steps WHERE plan_id = p.id AND status = 'completed') as completed_steps,
|
|||
|
|
* (SELECT COUNT(*) FROM inventory_records WHERE plan_id = p.id) as inventory_batches,
|
|||
|
|
* (SELECT COALESCE(SUM(quantity),0) FROM inventory_records WHERE plan_id = p.id) as total_meters
|
|||
|
|
* FROM production_plans p WHERE p.id = '{plan_id}';
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
test('验证测试数据结构完整性', async () => {
|
|||
|
|
// 此测试作为文档占位符,实际 SQL 验证通过 meoo-cli cloud query 执行
|
|||
|
|
// 预期结果:
|
|||
|
|
// - plan_code: TEST-2026-001
|
|||
|
|
// - status: completed
|
|||
|
|
// - target_quantity: 10000
|
|||
|
|
// - completed_quantity: 10000
|
|||
|
|
// - completed_steps: 5
|
|||
|
|
// - inventory_batches: 3
|
|||
|
|
// - total_meters: 10000
|
|||
|
|
expect(true).toBe(true);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('边界情况和异常处理', () => {
|
|||
|
|
|
|||
|
|
test('未登录用户访问受保护页面', async ({ page }) => {
|
|||
|
|
// 直接访问受保护页面
|
|||
|
|
await page.goto('/purchaser/plans');
|
|||
|
|
|
|||
|
|
// 应该被重定向到登录页
|
|||
|
|
await page.waitForURL(/.*login/, { timeout: 5000 });
|
|||
|
|
await expect(page).toHaveURL(/.*login/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('无效 token 导入计划', async ({ page }) => {
|
|||
|
|
await page.goto('/import?token=invalid-token');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 应该显示错误提示或重定向
|
|||
|
|
const hasError = await page.locator('text=错误, text=无效, text=失效, text=过期').count() > 0;
|
|||
|
|
const isRedirected = await page.url().includes('login') || await page.url().includes('error');
|
|||
|
|
|
|||
|
|
expect(hasError || isRedirected).toBeTruthy();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('网络断开时的离线提示', async ({ page }) => {
|
|||
|
|
await page.goto('/login');
|
|||
|
|
await waitForPageLoad(page);
|
|||
|
|
|
|||
|
|
// 模拟离线
|
|||
|
|
await page.context().setOffline(true);
|
|||
|
|
await page.waitForTimeout(1000);
|
|||
|
|
|
|||
|
|
// 检查是否有离线提示
|
|||
|
|
const hasOfflineNotice = await page.locator('text=离线, text=网络, text=断网').count() > 0;
|
|||
|
|
console.log(`离线提示检测: ${hasOfflineNotice ? '有' : '无'}`);
|
|||
|
|
|
|||
|
|
// 恢复网络
|
|||
|
|
await page.context().setOffline(false);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test.describe('性能测试', () => {
|
|||
|
|
|
|||
|
|
test('页面加载时间', async ({ page }) => {
|
|||
|
|
const startTime = Date.now();
|
|||
|
|
await page.goto('/login');
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
const loadTime = Date.now() - startTime;
|
|||
|
|
|
|||
|
|
console.log(`登录页加载时间: ${loadTime}ms`);
|
|||
|
|
|
|||
|
|
// 验证页面在合理时间内加载(5秒内)
|
|||
|
|
expect(loadTime).toBeLessThan(5000);
|
|||
|
|
|
|||
|
|
// 验证关键元素渲染
|
|||
|
|
await expect(page.locator('h1, [class*="title"]')).toBeVisible();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('资源加载无严重错误', async ({ page }) => {
|
|||
|
|
const errors: string[] = [];
|
|||
|
|
|
|||
|
|
page.on('console', (msg) => {
|
|||
|
|
if (msg.type() === 'error') {
|
|||
|
|
errors.push(msg.text());
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
page.on('pageerror', (error) => {
|
|||
|
|
errors.push(error.message);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
await page.goto('/login');
|
|||
|
|
await page.waitForLoadState('networkidle');
|
|||
|
|
|
|||
|
|
// 过滤预期的错误(如 API 调用、favicon 等)
|
|||
|
|
const unexpectedErrors = errors.filter(e =>
|
|||
|
|
!e.includes('supabase') &&
|
|||
|
|
!e.includes('api') &&
|
|||
|
|
!e.includes('favicon') &&
|
|||
|
|
!e.includes('404') &&
|
|||
|
|
!e.includes('Failed to fetch')
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
console.log(`检测到 ${unexpectedErrors.length} 个非预期错误`);
|
|||
|
|
if (unexpectedErrors.length > 0) {
|
|||
|
|
console.log('错误详情:', unexpectedErrors.slice(0, 3));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 允许少量非关键错误
|
|||
|
|
expect(unexpectedErrors.length).toBeLessThan(5);
|
|||
|
|
});
|
|||
|
|
});
|