Преглед изворни кода

test: 添加薪资模块和下拉刷新 E2E 测试

- 添加 salary-module Zod coerce 行为单元测试
- 添加数据统计页面下拉刷新 E2E 测试
- 添加薪资创建不选择区县场景 E2E 测试

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname пре 1 дан
родитељ
комит
6c800e24c9

+ 44 - 0
allin-packages/salary-module/tests/unit/zod-coerce-behavior.test.ts

@@ -0,0 +1,44 @@
+import { describe, it, expect } from 'vitest';
+import { z } from 'zod';
+
+describe('zod coerce.number 行为测试', () => {
+  it('应该验证空字符串被转换为 0 并触发 .positive() 验证错误', () => {
+    const schema = z.object({
+      districtId: z.coerce.number().int().positive().optional()
+    });
+
+    // 空字符串会被 coerce 转换为 0,然后 .positive() 验证失败
+    expect(() => schema.parse({ districtId: '' })).toThrow();
+  });
+
+  it('应该接受 undefined 值(因为 .optional())', () => {
+    const schema = z.object({
+      districtId: z.coerce.number().int().positive().optional()
+    });
+
+    const result = schema.parse({ districtId: undefined });
+    expect(result.districtId).toBeUndefined();
+  });
+
+  it('应该接受不传该字段(因为 .optional())', () => {
+    const schema = z.object({
+      districtId: z.coerce.number().int().positive().optional()
+    });
+
+    const result = schema.parse({});
+    expect(result.districtId).toBeUndefined();
+  });
+
+  it('空字符串被转换为 0 时抛出错误', () => {
+    const schema = z.object({
+      districtId: z.coerce.number().int().positive().optional()
+    });
+
+    try {
+      schema.parse({ districtId: '' });
+      expect.fail('应该抛出错误');
+    } catch (error) {
+      expect(error).toBeDefined();
+    }
+  });
+});

+ 283 - 0
web/tests/e2e/specs/mini/statistics-pull-refresh.spec.ts

@@ -0,0 +1,283 @@
+import { TIMEOUTS } from '../../utils/timeouts';
+import { test, expect } from '../../utils/test-setup';
+
+/**
+ * 数据统计页面下拉刷新功能测试
+ * 测试目标:验证数据统计页面的下拉刷新功能是否正常工作
+ */
+
+const TEST_USER_PHONE = '13800138005';
+const TEST_USER_PASSWORD = process.env.TEST_ENTERPRISE_PASSWORD || 'password123';
+
+test.describe('数据统计页面下拉刷新功能测试', () => {
+  test.use({ storageState: undefined });
+
+  test.beforeEach(async ({ enterpriseMiniPage: miniPage }) => {
+    await miniPage.goto();
+    await miniPage.login(TEST_USER_PHONE, TEST_USER_PASSWORD);
+    await miniPage.expectLoginSuccess();
+    await miniPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
+    await miniPage.navigateToStatisticsPage();
+    // 等待统计页面数据加载完成(不依赖 .stat-card 类名)
+    await miniPage.page.waitForTimeout(TIMEOUTS.LONG);
+  });
+
+  test('应该能够下拉刷新统计数据', async ({ enterpriseMiniPage: miniPage }) => {
+    console.debug('[测试] 开始下拉刷新测试');
+
+    // 1. 获取刷新前的统计页面文本内容
+    const beforeRefreshContent = await miniPage.page.textContent('body');
+    console.debug(`[测试] 刷新前页面内容长度: ${beforeRefreshContent?.length || 0}`);
+    expect(beforeRefreshContent).toBeTruthy();
+    expect(beforeRefreshContent).toContain('数据统计');
+    console.debug('[测试] ✓ 页面包含数据统计标题');
+
+    // 2. 验证初始数据存在
+    const hasInitialData = beforeRefreshContent?.includes('在职人数') ||
+                           beforeRefreshContent?.includes('平均薪资') ||
+                           beforeRefreshContent?.includes('在职率');
+    expect(hasInitialData).toBe(true);
+    console.debug('[测试] ✓ 初始统计数据存在');
+
+    // 3. 监听控制台错误
+    const consoleErrors: string[] = [];
+    miniPage.page.on('console', msg => {
+      if (msg.type() === 'error') {
+        consoleErrors.push(msg.text());
+      }
+    });
+
+    // 4. 执行下拉刷新动作
+    await miniPage.page.evaluate(async () => {
+      // 尝试找到可滚动元素并执行下拉刷新
+      const scrollableElements = [
+        document.querySelector('.h-\\[calc\\(100vh-120px\\)\\]'),
+        document.querySelector('[class*="overflow"]'),
+        document.querySelector('main'),
+        document.body
+      ].filter(el => el !== null) as HTMLElement[];
+
+      if (scrollableElements.length > 0) {
+        const scrollableElement = scrollableElements[0];
+        console.debug('[下拉刷新] 找到可滚动元素,执行刷新动作');
+
+        // 模拟下拉手势:先向下滚动,再快速滚回顶部
+        scrollableElement.scrollTop = 0;
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+
+        // 等待一小段时间后再次触发滚动事件
+        await new Promise(resolve => setTimeout(resolve, 100));
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+      } else {
+        console.warn('[下拉刷新] 未找到可滚动元素');
+      }
+    });
+
+    // 5. 等待刷新完成
+    await miniPage.page.waitForTimeout(TIMEOUTS.LONG);
+
+    // 6. 获取刷新后的页面内容
+    const afterRefreshContent = await miniPage.page.textContent('body');
+    console.debug(`[测试] 刷新后页面内容长度: ${afterRefreshContent?.length || 0}`);
+    expect(afterRefreshContent).toBeTruthy();
+
+    // 7. 验证页面仍然包含统计数据
+    const hasDataAfterRefresh = afterRefreshContent?.includes('在职人数') ||
+                                afterRefreshContent?.includes('平均薪资') ||
+                                afterRefreshContent?.includes('在职率');
+    expect(hasDataAfterRefresh).toBe(true);
+    console.debug('[测试] ✓ 刷新后统计数据仍然存在');
+
+    // 8. 验证页面没有被破坏
+    expect(afterRefreshContent).toContain('数据统计');
+    console.debug('[测试] ✓ 页面结构完整');
+
+    console.debug('[测试] ✅ 下拉刷新功能测试通过');
+  });
+
+  test('下拉刷新后应该重新加载数据', async ({ enterpriseMiniPage: miniPage }) => {
+    console.debug('[测试] 开始数据重新加载测试');
+
+    // 1. 等待初始数据加载
+    await miniPage.page.waitForTimeout(TIMEOUTS.MEDIUM);
+    const initialContent = await miniPage.page.textContent('body');
+    console.debug(`[测试] 初始页面内容长度: ${initialContent?.length || 0}`);
+    expect(initialContent).toContain('数据统计');
+    console.debug('[测试] ✓ 页面已加载');
+
+    // 2. 验证初始统计图表标题存在
+    const expectedCharts = ['残疾类型分布', '性别分布', '年龄分布', '户籍省份分布'];
+    const initialChartsFound = expectedCharts.filter(chart => initialContent?.includes(chart));
+    console.debug(`[测试] 初始找到 ${initialChartsFound.length}/${expectedCharts.length} 个图表标题`);
+    expect(initialChartsFound.length).toBeGreaterThan(0);
+    console.debug('[测试] ✓ 统计图表标题存在');
+
+    // 3. 截图 - 刷新前
+    await miniPage.page.screenshot({
+      path: 'test-results/statistics-before-refresh.png',
+      fullPage: true
+    });
+    console.debug('[测试] 已保存刷新前截图');
+
+    // 4. 执行下拉刷新
+    await miniPage.page.evaluate(async () => {
+      const scrollableElements = [
+        document.querySelector('.h-\\[calc\\(100vh-120px\\)\\]'),
+        document.querySelector('[class*="overflow"]'),
+        document.querySelector('main'),
+        document.body
+      ].filter(el => el !== null) as HTMLElement[];
+
+      if (scrollableElements.length > 0) {
+        const scrollableElement = scrollableElements[0];
+        scrollableElement.scrollTop = 0;
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+        await new Promise(resolve => setTimeout(resolve, 100));
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+      }
+    });
+
+    // 5. 等待数据重新加载
+    await miniPage.page.waitForTimeout(TIMEOUTS.LONG);
+
+    // 6. 验证数据仍然存在
+    const afterRefreshContent = await miniPage.page.textContent('body');
+    expect(afterRefreshContent).toContain('数据统计');
+
+    const afterRefreshChartsFound = expectedCharts.filter(chart => afterRefreshContent?.includes(chart));
+    console.debug(`[测试] 刷新后找到 ${afterRefreshChartsFound.length}/${expectedCharts.length} 个图表标题`);
+    expect(afterRefreshChartsFound.length).toBeGreaterThan(0);
+    console.debug('[测试] ✓ 刷新后图表标题仍然存在');
+
+    // 7. 截图 - 刷新后
+    await miniPage.page.screenshot({
+      path: 'test-results/statistics-after-refresh.png',
+      fullPage: true
+    });
+    console.debug('[测试] 已保存刷新后截图');
+
+    console.debug('[测试] ✅ 数据重新加载测试通过');
+  });
+
+  test('下拉刷新不应该产生控制台错误', async ({ enterpriseMiniPage: miniPage }) => {
+    console.debug('[测试] 开始控制台错误检查');
+
+    const consoleErrors: string[] = [];
+
+    // 监听控制台错误
+    miniPage.page.on('console', msg => {
+      if (msg.type() === 'error') {
+        const errorText = msg.text();
+        consoleErrors.push(errorText);
+        console.debug(`[控制台错误] ${errorText}`);
+      }
+    });
+
+    // 执行下拉刷新
+    await miniPage.page.evaluate(async () => {
+      const scrollableElements = [
+        document.querySelector('.h-\\[calc\\(100vh-120px\\)\\]'),
+        document.querySelector('[class*="overflow"]'),
+        document.querySelector('main'),
+        document.body
+      ].filter(el => el !== null) as HTMLElement[];
+
+      if (scrollableElements.length > 0) {
+        const scrollableElement = scrollableElements[0];
+        scrollableElement.scrollTop = 0;
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+        await new Promise(resolve => setTimeout(resolve, 100));
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+      }
+    });
+
+    await miniPage.page.waitForTimeout(TIMEOUTS.LONG);
+
+    // 验证没有控制台错误
+    // 过滤掉一些常见的非关键错误(如资源加载失败)
+    const criticalErrors = consoleErrors.filter(err => {
+      return !err.includes('net::ERR_FAILED') &&
+             !err.includes('404') &&
+             !err.includes('Failed to load resource');
+    });
+
+    if (criticalErrors.length > 0) {
+      console.debug(`[测试] 发现 ${criticalErrors.length} 个控制台错误`);
+      console.debug(`[测试] 错误详情: ${criticalErrors.join(', ')}`);
+    }
+
+    // 只有关键错误才导致测试失败
+    expect(criticalErrors.length).toBe(0);
+    console.debug('[测试] ✅ 无关键控制台错误');
+  });
+
+  test('完整下拉刷新流程验证', async ({ enterpriseMiniPage: miniPage }) => {
+    console.debug('[测试] 开始完整下拉刷新流程验证');
+
+    // 1. 验证初始状态
+    const initialContent = await miniPage.page.textContent('body');
+    console.debug(`[测试] 初始页面内容长度: ${initialContent?.length || 0}`);
+    expect(initialContent).toContain('数据统计');
+    console.debug('[测试] ✓ 页面标题正确');
+
+    // 验证初始统计数据
+    const initialDataExists = initialContent?.includes('在职人数') ||
+                              initialContent?.includes('平均薪资') ||
+                              initialContent?.includes('在职率');
+    expect(initialDataExists).toBe(true);
+    console.debug('[测试] ✓ 初始统计数据存在');
+
+    // 2. 执行下拉刷新
+    await miniPage.page.evaluate(async () => {
+      const scrollableElements = [
+        document.querySelector('.h-\\[calc\\(100vh-120px\\)\\]'),
+        document.querySelector('[class*="overflow"]'),
+        document.querySelector('main'),
+        document.body
+      ].filter(el => el !== null) as HTMLElement[];
+
+      if (scrollableElements.length > 0) {
+        const scrollableElement = scrollableElements[0];
+        // 模拟下拉手势:先向下滚动,再快速滚回顶部
+        scrollableElement.scrollTop = 100;
+        await new Promise(resolve => setTimeout(resolve, 50));
+        scrollableElement.scrollTop = 0;
+
+        // 触发滚动事件
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+        await new Promise(resolve => setTimeout(resolve, 100));
+        scrollableElement.dispatchEvent(new Event('scroll', { bubbles: true }));
+      }
+    });
+
+    // 3. 等待刷新完成
+    await miniPage.page.waitForTimeout(TIMEOUTS.LONG);
+
+    // 4. 验证刷新后状态
+    const afterRefreshContent = await miniPage.page.textContent('body');
+    console.debug(`[测试] 刷新后页面内容长度: ${afterRefreshContent?.length || 0}`);
+    expect(afterRefreshContent).toContain('数据统计');
+    console.debug('[测试] ✓ 刷新后页面标题正确');
+
+    // 验证刷新后统计数据仍然存在
+    const afterDataExists = afterRefreshContent?.includes('在职人数') ||
+                            afterRefreshContent?.includes('平均薪资') ||
+                            afterRefreshContent?.includes('在职率');
+    expect(afterDataExists).toBe(true);
+    console.debug('[测试] ✓ 刷新后统计数据仍然存在');
+
+    // 5. 验证页面结构完整性
+    const expectedElements = ['残疾类型分布', '性别分布'];
+    const foundElements = expectedElements.filter(el => afterRefreshContent?.includes(el));
+    expect(foundElements.length).toBeGreaterThan(0);
+    console.debug(`[测试] ✓ 找到 ${foundElements.length}/${expectedElements.length} 个预期元素`);
+
+    // 6. 最终截图
+    await miniPage.page.screenshot({
+      path: 'test-results/statistics-pull-refresh-complete.png',
+      fullPage: true
+    });
+
+    console.debug('[测试] ✅ 完整下拉刷新流程验证通过');
+  });
+});

+ 131 - 0
web/tests/e2e/specs/salary-no-district.spec.ts

@@ -0,0 +1,131 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('薪资创建测试 - 不选择区县', () => {
+  test('应该能够在不选择区县的情况下成功创建薪资', async ({ page }) => {
+    // 监听控制台消息
+    page.on('console', msg => {
+      if (msg.type() === 'error') {
+        console.debug('[控制台错误]', msg.text());
+      }
+    });
+
+    // 导航到薪资管理页面
+    console.debug('导航到薪资管理页面...');
+    await page.goto('/admin/salaries');
+    await page.waitForLoadState('networkidle');
+    
+    // 截图 - 初始页面
+    await page.screenshot({ path: 'test-results/salary-01-initial.png' });
+
+    // 检查是否需要登录
+    const hasPasswordField = await page.getByRole('textbox', { name: '请输入密码' }).isVisible({ timeout: 3000 }).catch(() => false);
+    if (hasPasswordField) {
+      console.debug('检测到登录表单,正在登录...');
+      await page.getByRole('textbox', { name: '请输入用户名' }).fill('admin');
+      await page.getByRole('textbox', { name: '请输入密码' }).fill('admin123');
+      await page.getByRole('button', { name: '登录' }).click();
+      await page.waitForLoadState('networkidle');
+      console.debug('登录成功');
+    }
+
+    await page.waitForTimeout(2000);
+    await page.screenshot({ path: 'test-results/salary-02-after-login.png' });
+
+    // 点击"薪资管理"导航按钮
+    console.debug('点击薪资管理导航按钮...');
+    const salaryNavButton = page.getByRole('button', { name: '薪资管理' });
+    await salaryNavButton.click({ timeout: 5000 });
+    await page.waitForLoadState('networkidle');
+    await page.waitForTimeout(1000);
+    await page.screenshot({ path: 'test-results/salary-03-nav-salary.png' });
+
+    // 点击"添加薪资"按钮
+    console.debug('点击添加薪资按钮...');
+    const addButton = page.locator('button').filter({ hasText: /添加薪资/ }).first();
+    await addButton.click({ timeout: 10000 });
+    
+    await page.waitForTimeout(1500);
+    await page.screenshot({ path: 'test-results/salary-04-click-add.png' });
+
+    // 在对话框中选择省份 - 点击省份 combobox
+    console.debug('选择省份: 北京市');
+    const provinceCombobox = page.getByRole('combobox', { name: '省份 *' });
+    await provinceCombobox.click({ timeout: 5000 });
+    await page.waitForTimeout(500);
+    
+    // 点击北京市选项
+    const beijingOption = page.locator('[role="option"]').filter({ hasText: '北京市' }).first();
+    await beijingOption.click({ timeout: 5000 });
+    console.debug('已选择北京市');
+    
+    await page.waitForTimeout(1000);
+    await page.screenshot({ path: 'test-results/salary-05-province.png' });
+
+    // 选择城市 - 点击城市 combobox
+    console.debug('选择城市: 朝阳区');
+    const cityCombobox = page.getByRole('combobox', { name: '城市' });
+    await cityCombobox.click({ timeout: 5000 });
+    await page.waitForTimeout(500);
+    
+    // 点击朝阳区选项
+    const chaoyangOption = page.locator('[role="option"]').filter({ hasText: '朝阳区' }).first();
+    await chaoyangOption.click({ timeout: 5000 });
+    console.debug('已选择朝阳区');
+    
+    await page.waitForTimeout(1000);
+    await page.screenshot({ path: 'test-results/salary-06-city.png' });
+
+    // 关键: 不选择区县
+    console.debug('跳过区县选择 (测试目标: 验证不选择区县也能创建)');
+    await page.waitForTimeout(500);
+
+    // 填写基本工资
+    console.debug('填写基本工资: 5000');
+    const salaryInput = page.getByRole('spinbutton', { name: '基本工资' });
+    await salaryInput.fill('5000');
+    console.debug('已填写基本工资: 5000');
+    
+    await page.waitForTimeout(500);
+    await page.screenshot({ path: 'test-results/salary-07-form-filled.png' });
+
+    // 点击创建薪资按钮
+    console.debug('点击创建薪资按钮...');
+    const createButton = page.getByRole('button', { name: '创建薪资' });
+    await createButton.click({ timeout: 5000 });
+    console.debug('已点击创建薪资按钮');
+
+    // 等待结果
+    await page.waitForTimeout(3000);
+    await page.screenshot({ path: 'test-results/salary-08-result.png' });
+
+    // 检查结果
+    console.debug('='.repeat(50));
+    console.debug('测试结果分析:');
+    
+    // 检查是否有验证错误
+    const errors = page.locator('.error, .validation-error, .ant-form-item-explain-error, [data-invalid="true"]');
+    const errorCount = await errors.count();
+    
+    let hasDistrictError = false;
+    for (let i = 0; i < errorCount; i++) {
+      const text = await errors.nth(i).textContent();
+      if (text && text.includes('区县')) {
+        hasDistrictError = true;
+        console.debug('发现区县验证错误: ' + text);
+      }
+    }
+    
+    // 检查对话框是否关闭
+    const dialogVisible = await page.getByRole('dialog', { name: '添加薪资' }).isVisible({ timeout: 1000 }).catch(() => false);
+    console.debug('对话框状态: ' + (dialogVisible ? '仍显示' : '已关闭'));
+    
+    // 最终截图
+    await page.screenshot({ path: 'test-results/salary-final.png', fullPage: true });
+    
+    console.debug('='.repeat(50));
+    
+    // 断言: 不应该有关于区县的验证错误
+    expect(hasDistrictError, '不应该有区县相关的验证错误').toBe(false);
+    console.debug('测试通过: 不选择区县也能提交表单');
+  });
+});