|
|
@@ -11,7 +11,7 @@ const MINI_LOGIN_URL = `${MINI_BASE_URL}/talent-mini`;
|
|
|
* Token 存储键名(人才小程序专用)
|
|
|
*/
|
|
|
const TOKEN_KEY = 'talent_token';
|
|
|
-const _USER_KEY = 'talent_user';
|
|
|
+const USER_KEY = 'talent_user';
|
|
|
|
|
|
/**
|
|
|
* 人才小程序 Page Object
|
|
|
@@ -117,33 +117,39 @@ export class TalentMiniPage {
|
|
|
/**
|
|
|
* 填写身份标识(手机号/身份证号/残疾证号)
|
|
|
* @param identifier 身份标识(11位手机号或身份证号或残疾证号)
|
|
|
+ *
|
|
|
+ * 注意:使用 focus + type 方法触发自然的用户输入事件
|
|
|
+ * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
|
|
|
*/
|
|
|
async fillIdentifier(identifier: string): Promise<void> {
|
|
|
- // Taro 的 Input 组件使用自定义元素,使用 JavaScript 直接设置值
|
|
|
- // 不使用 click() 避免被开发服务器的覆盖层阻止
|
|
|
- await this.identifierInput.evaluate((el: HTMLInputElement, value) => {
|
|
|
- el.value = value;
|
|
|
- el.focus();
|
|
|
- el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
- el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- el.blur();
|
|
|
- }, identifier);
|
|
|
+ // 先移除覆盖层,确保输入可操作
|
|
|
+ await this.removeDevOverlays();
|
|
|
+ // 先清空输入框
|
|
|
+ await this.identifierInput.evaluate((el: HTMLInputElement | { value: string }) => {
|
|
|
+ el.value = '';
|
|
|
+ });
|
|
|
+ // 使用 focus + type 方法,触发自然的键盘输入事件
|
|
|
+ await this.identifierInput.focus();
|
|
|
+ await this.identifierInput.type(identifier, { delay: 10 });
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 填写密码
|
|
|
* @param password 密码(6-20位)
|
|
|
+ *
|
|
|
+ * 注意:使用 focus + type 方法触发自然的用户输入事件
|
|
|
+ * Taro Input 组件需要完整的事件流才能正确更新 react-hook-form 状态
|
|
|
*/
|
|
|
async fillPassword(password: string): Promise<void> {
|
|
|
- // Taro 的 Input 组件使用自定义元素,使用 JavaScript 直接设置值
|
|
|
- // 不使用 click() 避免被开发服务器的覆盖层阻止
|
|
|
- await this.passwordInput.evaluate((el: HTMLInputElement, value) => {
|
|
|
- el.value = value;
|
|
|
- el.focus();
|
|
|
- el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
|
- el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
- el.blur();
|
|
|
- }, password);
|
|
|
+ // 先移除覆盖层,确保输入可操作
|
|
|
+ await this.removeDevOverlays();
|
|
|
+ // 先清空输入框
|
|
|
+ await this.passwordInput.evaluate((el: HTMLInputElement | { value: string }) => {
|
|
|
+ el.value = '';
|
|
|
+ });
|
|
|
+ // 使用 focus + type 方法,触发自然的键盘输入事件
|
|
|
+ await this.passwordInput.focus();
|
|
|
+ await this.passwordInput.type(password, { delay: 10 });
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -188,25 +194,35 @@ export class TalentMiniPage {
|
|
|
/**
|
|
|
* 验证登录失败(错误提示显示)
|
|
|
* @param expectedErrorMessage 预期的错误消息(可选)
|
|
|
+ * @param options 配置选项
|
|
|
+ * @param options.requireErrorMessage 是否要求错误消息必须可见(默认为 false)
|
|
|
*/
|
|
|
- async expectLoginError(expectedErrorMessage?: string): Promise<void> {
|
|
|
+ async expectLoginError(
|
|
|
+ expectedErrorMessage?: string,
|
|
|
+ options: { requireErrorMessage?: boolean } = {}
|
|
|
+ ): Promise<void> {
|
|
|
+ const { requireErrorMessage = false } = options;
|
|
|
+
|
|
|
// 等待一下,让后端响应或前端验证生效
|
|
|
- await this.page.waitForTimeout(TIMEOUTS.MEDIUM);
|
|
|
+ await this.page.waitForTimeout(1000);
|
|
|
|
|
|
// 验证仍然在登录页面(未跳转)
|
|
|
const currentUrl = this.page.url();
|
|
|
expect(currentUrl).toContain('/talent-mini');
|
|
|
|
|
|
// 验证登录页面元素仍然可见
|
|
|
- await expect(this.identifierInput).toBeVisible();
|
|
|
- await expect(this.passwordInput).toBeVisible();
|
|
|
+ await expect(this.loginPage).toBeVisible();
|
|
|
|
|
|
// 如果提供了预期的错误消息,尝试验证
|
|
|
if (expectedErrorMessage) {
|
|
|
// 尝试查找错误消息(可能在 Toast、Modal 或表单验证中)
|
|
|
const errorElement = this.page.getByText(expectedErrorMessage, { exact: false }).first();
|
|
|
- await errorElement.isVisible().catch(() => false);
|
|
|
- // 不强制要求错误消息可见,因为后端可能不会返回错误
|
|
|
+ const isVisible = await errorElement.isVisible().catch(() => false);
|
|
|
+
|
|
|
+ // 如果要求错误消息必须可见,则进行断言
|
|
|
+ if (requireErrorMessage) {
|
|
|
+ expect(isVisible).toBe(true);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -215,22 +231,37 @@ export class TalentMiniPage {
|
|
|
/**
|
|
|
* 获取当前存储的 token
|
|
|
* @returns token 字符串,如果不存在则返回 null
|
|
|
+ *
|
|
|
+ * 注意:Taro.getStorageSync 在 H5 环境下映射到 localStorage
|
|
|
+ * token 直接存储为字符串,不是 JSON 格式
|
|
|
+ *
|
|
|
+ * Taro H5 可能使用以下键名格式:
|
|
|
+ * - 直接键名: 'talent_token'
|
|
|
+ * - 带前缀: 'taro_app_storage_key'
|
|
|
+ * - 或者其他变体
|
|
|
*/
|
|
|
async getToken(): Promise<string | null> {
|
|
|
const result = await this.page.evaluate(() => {
|
|
|
- // 从 talent_token 获取(人才小程序专用)
|
|
|
- const talentToken = localStorage.getItem('talent_token');
|
|
|
- if (talentToken) {
|
|
|
- try {
|
|
|
- const parsed = JSON.parse(talentToken);
|
|
|
- return parsed.data || talentToken;
|
|
|
- } catch {
|
|
|
- // 如果解析失败,直接返回原字符串
|
|
|
- return talentToken;
|
|
|
+ // 获取所有 localStorage 键
|
|
|
+ const keys = Object.keys(localStorage);
|
|
|
+ const storage: Record<string, string> = {};
|
|
|
+ keys.forEach(k => storage[k] = localStorage.getItem(k) || '');
|
|
|
+
|
|
|
+ // 尝试各种可能的键名
|
|
|
+ // 1. 直接键名(人才小程序专用)
|
|
|
+ const token = localStorage.getItem('talent_token');
|
|
|
+ if (token) return token;
|
|
|
+
|
|
|
+ // 2. 带前缀的键名(Taro 可能使用前缀)
|
|
|
+ const prefixedKeys = keys.filter(k => k.includes('token') || k.includes('auth'));
|
|
|
+ for (const key of prefixedKeys) {
|
|
|
+ const value = localStorage.getItem(key);
|
|
|
+ if (value && value.length > 20) { // JWT token 通常很长
|
|
|
+ return value;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 尝试其他常见 token 键
|
|
|
+ // 3. 其他常见键名
|
|
|
return (
|
|
|
localStorage.getItem('token') ||
|
|
|
localStorage.getItem('auth_token') ||
|
|
|
@@ -261,7 +292,7 @@ export class TalentMiniPage {
|
|
|
await this.page.evaluate(() => {
|
|
|
// 清除人才小程序相关的认证数据
|
|
|
localStorage.removeItem('talent_token');
|
|
|
- localStorage.removeItem('talent_user');
|
|
|
+ localStorage.removeItem(USER_KEY);
|
|
|
|
|
|
// 清除其他常见 token 键
|
|
|
localStorage.removeItem('token');
|
|
|
@@ -271,16 +302,46 @@ export class TalentMiniPage {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 验证 token 持久性(AC4)
|
|
|
+ *
|
|
|
+ * 用于验证登录后 token 被正确存储,并且页面刷新后仍然有效
|
|
|
+ * 测试步骤:
|
|
|
+ * 1. 获取当前 token
|
|
|
+ * 2. 刷新页面
|
|
|
+ * 3. 再次获取 token,确认与刷新前相同
|
|
|
+ *
|
|
|
+ * @returns Promise<boolean> 如果 token 持久性验证通过返回 true
|
|
|
+ */
|
|
|
+ async expectTokenPersistence(): Promise<boolean> {
|
|
|
+ // 获取刷新前的 token
|
|
|
+ const tokenBefore = await this.getToken();
|
|
|
+
|
|
|
+ // 刷新页面
|
|
|
+ await this.page.reload();
|
|
|
+ await this.page.waitForLoadState('domcontentloaded');
|
|
|
+
|
|
|
+ // 获取刷新后的 token
|
|
|
+ const tokenAfter = await this.getToken();
|
|
|
+
|
|
|
+ // 验证 token 相同
|
|
|
+ return tokenBefore === tokenAfter && tokenBefore !== null;
|
|
|
+ }
|
|
|
+
|
|
|
// ===== 主页元素验证方法 =====
|
|
|
|
|
|
/**
|
|
|
* 验证主页元素可见(登录后)
|
|
|
* 根据实际小程序主页结构调整
|
|
|
+ *
|
|
|
+ * 注意:此方法需要在主页实现后添加对应的 data-testid
|
|
|
+ * 当前使用的 'talent-dashboard' 选择器需要在主页中实现
|
|
|
+ * 主页实现位置:mini/src/pages/dashboard/index.tsx
|
|
|
*/
|
|
|
async expectHomePageVisible(): Promise<void> {
|
|
|
// 使用 auto-waiting 机制,等待主页元素可见
|
|
|
// 注意:此方法将在 Story 12.7 E2E 测试中使用,当前仅提供基础结构
|
|
|
- // 根据实际小程序主页的 data-testid 调整
|
|
|
+ // TODO: 根据实际小程序主页的 data-testid 调整
|
|
|
const dashboard = this.page.getByTestId('talent-dashboard');
|
|
|
await dashboard.waitFor({ state: 'visible', timeout: TIMEOUTS.PAGE_LOAD });
|
|
|
}
|