Просмотр исходного кода

fix(story-12.5): 修复 Page Object token 解析和测试问题

修复内容:
- getToken(): 正确解析 Taro 存储格式 {"data":"JWT_TOKEN"}
- expectLoginSuccess(): 失败时抛出错误而非静默继续
- 修复公司名称: 默认公司 -> 测试公司_E2E
- test-setup.ts: 修复 fixture 参数问题 (ESLint)

注意: 之前对 useAuth.tsx 的修改已回滚,应用层代码正常

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname 5 дней назад
Родитель
Сommit
fa79190e28

+ 75 - 32
web/tests/e2e/pages/mini/enterprise-mini.page.ts

@@ -114,7 +114,7 @@ export class EnterpriseMiniPage {
    * 填写手机号
    * 填写手机号
    * @param phone 手机号(11位数字)
    * @param phone 手机号(11位数字)
    *
    *
-   * 注意:使用 click + type 方法触发自然的用户输入事件
+   * 注意:使用 fill() 方法并添加验证步骤确保密码输入完整
    * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
    * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
    */
    */
   async fillPhone(phone: string): Promise<void> {
   async fillPhone(phone: string): Promise<void> {
@@ -138,16 +138,24 @@ export class EnterpriseMiniPage {
    * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
    * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
    */
    */
   async fillPassword(password: string): Promise<void> {
   async fillPassword(password: string): Promise<void> {
-    // 先移除覆盖层,确保输入可操作
     await this.removeDevOverlays();
     await this.removeDevOverlays();
-    // 点击聚焦
     await this.passwordInput.click();
     await this.passwordInput.click();
-    // 等待元素聚焦
     await this.page.waitForTimeout(100);
     await this.page.waitForTimeout(100);
-    // 使用 type 方法输入
-    await this.passwordInput.type(password, { delay: 50 });
-    // 等待表单验证更新
-    await this.page.waitForTimeout(200);
+    // 使用 fill 方法一次性填充
+    await this.passwordInput.fill(password);
+    await this.page.waitForTimeout(300);
+    // 验证输入值
+    const actualValue = await this.passwordInput.inputValue();
+    if (actualValue !== password) {
+      // 使用 JS 直接设置
+      await this.passwordInput.evaluate((el, val) => {
+        const input = el as HTMLInputElement;
+        input.value = val;
+        input.dispatchEvent(new Event('input', { bubbles: true }));
+        input.dispatchEvent(new Event('change', { bubbles: true }));
+      }, password);
+      await this.page.waitForTimeout(200);
+    }
   }
   }
 
 
   /**
   /**
@@ -179,14 +187,18 @@ export class EnterpriseMiniPage {
     // 小程序登录成功后会跳转到 dashboard 页面
     // 小程序登录成功后会跳转到 dashboard 页面
 
 
     // 等待 URL 变化,使用 Promise.race 实现超时
     // 等待 URL 变化,使用 Promise.race 实现超时
-    await this.page.waitForURL(
+    const urlChanged = await this.page.waitForURL(
       url => url.pathname.includes('/dashboard') || url.pathname.includes('/pages/yongren/dashboard'),
       url => url.pathname.includes('/dashboard') || url.pathname.includes('/pages/yongren/dashboard'),
       { timeout: TIMEOUTS.PAGE_LOAD }
       { timeout: TIMEOUTS.PAGE_LOAD }
-    ).catch(() => {
-      // 如果没有跳转,检查是否显示用户信息
-      // 注意:此验证将在 Story 12.5 E2E 测试中完全实现
-      // 当前仅提供基础结构
-    });
+    ).then(() => true).catch(() => false);
+
+    // 如果 URL 没有变化,检查 token 是否被存储
+    if (!urlChanged) {
+      const token = await this.getToken();
+      if (!token) {
+        throw new Error('登录失败:URL 未跳转且 token 未存储');
+      }
+    }
   }
   }
 
 
   /**
   /**
@@ -220,7 +232,8 @@ export class EnterpriseMiniPage {
    * @returns token 字符串,如果不存在则返回 null
    * @returns token 字符串,如果不存在则返回 null
    *
    *
    * 注意:Taro.getStorageSync 在 H5 环境下映射到 localStorage
    * 注意:Taro.getStorageSync 在 H5 环境下映射到 localStorage
-   * token 直接存储为字符串,不是 JSON 格式
+   * Taro.setStorageSync 会将数据包装为 JSON 格式:{"data":"VALUE"}
+   * 因此需要解析 JSON 并提取 data 字段
    *
    *
    * Taro H5 可能使用以下键名格式:
    * Taro H5 可能使用以下键名格式:
    * - 直接键名: 'enterprise_token'
    * - 直接键名: 'enterprise_token'
@@ -229,33 +242,63 @@ export class EnterpriseMiniPage {
    */
    */
   async getToken(): Promise<string | null> {
   async getToken(): Promise<string | null> {
     const result = await this.page.evaluate(() => {
     const result = await this.page.evaluate(() => {
-      // 获取所有 localStorage 键
-      const keys = Object.keys(localStorage);
-      const storage: Record<string, string> = {};
-      keys.forEach(k => storage[k] = localStorage.getItem(k) || '');
-
       // 尝试各种可能的键名
       // 尝试各种可能的键名
-      // 1. 直接键名
+      // 1. 直接键名 - Taro 的 setStorageSync 将数据包装为 {"data":"VALUE"}
       const token = localStorage.getItem('enterprise_token');
       const token = localStorage.getItem('enterprise_token');
-      if (token) return token;
+      if (token) {
+        try {
+          // Taro 格式: {"data":"JWT_TOKEN"}
+          const parsed = JSON.parse(token);
+          if (parsed.data) {
+            return parsed.data;
+          }
+          return token;
+        } catch {
+          return token;
+        }
+      }
 
 
-      // 2. 带前缀的键名(Taro 可能使用前缀)
+      // 2. 获取所有 localStorage 键,查找可能的 token
+      const keys = Object.keys(localStorage);
       const prefixedKeys = keys.filter(k => k.includes('token') || k.includes('auth'));
       const prefixedKeys = keys.filter(k => k.includes('token') || k.includes('auth'));
       for (const key of prefixedKeys) {
       for (const key of prefixedKeys) {
         const value = localStorage.getItem(key);
         const value = localStorage.getItem(key);
-        if (value && value.length > 20) { // JWT token 通常很长
-          return value;
+        if (value) {
+          try {
+            // 尝试解析 Taro 格式
+            const parsed = JSON.parse(value);
+            if (parsed.data && parsed.data.length > 20) { // JWT token 通常很长
+              return parsed.data;
+            }
+          } catch {
+            // 不是 JSON 格式,直接使用
+            if (value.length > 20) {
+              return value;
+            }
+          }
         }
         }
       }
       }
 
 
       // 3. 其他常见键名
       // 3. 其他常见键名
-      return (
-        localStorage.getItem('token') ||
-        localStorage.getItem('auth_token') ||
-        sessionStorage.getItem('token') ||
-        sessionStorage.getItem('auth_token') ||
-        null
-      );
+      const otherTokens = [
+        localStorage.getItem('token'),
+        localStorage.getItem('auth_token'),
+        sessionStorage.getItem('token'),
+        sessionStorage.getItem('auth_token')
+      ].filter(Boolean);
+
+      for (const t of otherTokens) {
+        if (t) {
+          try {
+            const parsed = JSON.parse(t);
+            if (parsed.data) return parsed.data;
+          } catch {
+            if (t.length > 20) return t;
+          }
+        }
+      }
+
+      return null;
     });
     });
     return result;
     return result;
   }
   }

+ 1 - 1
web/tests/e2e/specs/mini/enterprise-mini-login.spec.ts

@@ -61,7 +61,7 @@ async function createTestUser(browser: typeof test['fixtures']['browser'], userD
       phone: userData.phone,
       phone: userData.phone,
       email: userData.email,
       email: userData.email,
       userType: userData.userType || UserType.EMPLOYER,
       userType: userData.userType || UserType.EMPLOYER,
-    }, userData.companyName || '默认公司');
+    }, userData.companyName || '测试公司_E2E');
 
 
     // 验证创建成功
     // 验证创建成功
     expect(result.success).toBe(true);
     expect(result.success).toBe(true);

+ 2 - 1
web/tests/e2e/utils/test-setup.ts

@@ -71,7 +71,8 @@ export const test = base.extend<Fixtures>({
   talentMiniPage: async ({ page }, use) => {
   talentMiniPage: async ({ page }, use) => {
     await use(new TalentMiniPage(page));
     await use(new TalentMiniPage(page));
   },
   },
-  testUsers: async ({ _ }, use) => {
+  // 自定义 fixture 不需要依赖其他 fixtures 时,使用空对象参数
+  testUsers: async (_fixtures: unknown, use) => {
     await use(testUsers);
     await use(testUsers);
   },
   },
 });
 });