AI_UIAutomation/utils/common/feedback.helper.ts

150 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { DeviceDriver } from '../../drivers/types';
import { sleep, waitForSource } from './element.helper';
import { extractTicketId, setSharedTicketId } from './zendesk.helper';
import { goToProfile } from './auth.helper';
async function tapText(driver: DeviceDriver, t: string): Promise<boolean> {
// iOS:按 **name 或 label** 匹配(有些按钮如 "Feedback" name 为空、文字在 label;只按 name 会漏)。
const el = driver.platform === 'android'
? await driver.findElementRaw('-android uiautomator', `new UiSelector().textContains("${t}")`)
: (await driver.findElementRaw('name', t).catch(() => null))
|| (await driver.findElementRaw('predicate string', `name == "${t}" OR label == "${t}" OR name CONTAINS "${t}" OR label CONTAINS "${t}"`).catch(() => null));
if (el) { await driver.tapElement(el); return true; }
return false;
}
/** Profile → Support → Feedback 页(我的反馈工单列表 + Create a New Ticket)。 */
export async function navigateToFeedback(driver: DeviceDriver): Promise<boolean> {
// 进 Profile:iOS 用 goToProfile(点 Profile tab,稳;Android descriptionContains 在 iOS 不可靠 → 进不去 → 找不到 Support)
if (driver.platform === 'ios') {
await goToProfile(driver); await sleep(1500);
} else {
const prof = await driver.findElementRaw('-android uiautomator', 'new UiSelector().descriptionContains("Profile")');
if (prof) { await driver.tapElement(prof); await sleep(2000); }
}
if (!(await tapText(driver, 'Support'))) return false;
await sleep(2000);
// Feedback 入口:Android 文字 "Feedback";**iOS 的 Feedback 按钮 name/label 全空**(文字在子节点/图标),
// tapText 匹配不到 → 它是 "Contact Us" **正上方**那个等宽 Button(实测 Rapid Replacement / [Feedback] / Contact Us 三个等宽按钮叠放),按相对位置定位。
if (driver.platform === 'ios') {
let contact = null;
for (let i = 0; i < 8 && !contact; i++) { contact = await driver.findElementRaw('name', 'Contact Us').catch(() => null); if (!contact) await sleep(1000); } // 等 Support 页底部渲染(消除 true→false 抖动)
if (!contact) { console.log('FB-FAIL: iOS Support 页未找到 Contact Us(无法定位上方 Feedback)'); return false; }
const cr = await driver.getElementRect(contact);
const btns = await driver.findElementsRaw('predicate string', 'type == "XCUIElementTypeButton"').catch(() => [] as string[]);
let fb: { x: number; y: number; width: number; height: number } | null = null;
for (const b of btns) { const r = await driver.getElementRect(b).catch(() => null); if (r && r.width > 300 && r.y < cr.y && (!fb || r.y > fb.y)) fb = r; } // Contact Us 正上方那个等宽按钮
if (!fb) { console.log('FB-FAIL: iOS 未定位到 Contact Us 上方的 Feedback 按钮'); return false; }
await driver.tap(Math.round(fb.x + fb.width / 2), Math.round(fb.y + fb.height / 2));
} else {
if (!(await tapText(driver, 'Feedback'))) return false;
}
await sleep(2000);
return true;
}
/**
* 提交一条 feedback 工单(Android 实测 2026-06)。流程:
* Feedback 页 → Create a New Ticket → Question Type 滚轮(从 Product Issues 起第 typeIndex 个)
* → 填 Question Details → Submit → "My Feedback" 成功页(含 Ticket ID)。
*
* Question Type 滚轮选项无障碍不可见,只能盲滑:**先重置到顶(Product Issues),再上滑 typeIndex 格**。
* 顺序:0 Product Issues / 1 Order Tracking / 2 Buying Guide / 3 App issues / 4 Rapid Replacement / 5 B2B Enquiry。
* 坐标按 1080×2280;换机型需调。
* @returns 是否到达成功页
*/
export async function submitFeedback(
driver: DeviceDriver,
opts: { details?: string; typeIndex?: number } = {}
): Promise<boolean> {
const details = opts.details || '内测问题直接关闭插件热更校验';
const typeIndex = opts.typeIndex ?? 3; // App issues
// 点 "Create a New Ticket" 进建单表单。**偶现误点到列表里某条工单→进工单详情页**(列表渲染/位置抖动)→
// 优先点 Create **按钮元素**(iOS 按 Button 类型+名,避开列表项),并**校验是否到表单(Question Type)**;没到=误点进详情→返回重试。
let onForm = false;
for (let attempt = 0; attempt < 3 && !onForm; attempt++) {
const cbtn = driver.platform === 'ios'
? await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeButton" AND (name == "Create a New Ticket" OR label == "Create a New Ticket")').catch(() => null)
: null;
if (cbtn) await driver.tapElement(cbtn);
else if (!(await tapText(driver, 'Create a New Ticket'))) { if (!(await tapText(driver, 'Create'))) { console.log('FB-FAIL: 无 Create a New Ticket/Create 按钮'); return false; } }
onForm = (await waitForSource(driver, 'Question Type', 8000).catch(() => false)) as boolean;
if (!onForm) { console.log(`FB: Create 后未到建单表单(疑误点进工单详情),返回重试 ${attempt + 1}/3`); await driver.goBack().catch(() => {}); await sleep(1500); }
}
if (!onForm) { console.log('FB-FAIL: 多次点 Create 仍未进建单表单'); return false; }
console.log('FB: 已进建单表单');
if (driver.platform === 'ios') {
// iOS:Question Type 是**级联下拉**(主类型 + 子类型,未选时都显示 "Please select")。
// 逐个选:找 value=="Please select" 的 TextField → 点开 PickerWheel → 选 + Confirm(轮询到 picker 关闭),直到没有未选的。
for (let round = 0; round < 3; round++) {
const ps = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeTextField" AND value == "Please select"').catch(() => null);
if (!ps) break;
const r = await driver.getElementRect(ps);
await driver.tap(Math.round(r.x + r.width / 2), Math.round(r.y + r.height / 2)); await sleep(1500);
let wheel = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypePickerWheel"').catch(() => null);
for (let i = 0; i < 6 && !wheel; i++) { await sleep(800); wheel = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypePickerWheel"').catch(() => null); }
if (round === 0 && wheel) { await driver.setPickerWheelValue?.(wheel, 'App issues')?.catch(() => {}); await sleep(800); } // 主类型选 App issues(滚轮整串设值,typeText 逐字符对 picker 无效);子类型用默认首项
let closed = false;
for (let i = 0; i < 6 && !closed; i++) { await tapText(driver, 'Confirm'); await sleep(1000); closed = !(await driver.findElementRaw('predicate string', 'type == "XCUIElementTypePickerWheel"').catch(() => null)); }
if (!closed) { console.log('FB-FAIL: iOS Question Type picker 未关闭(Confirm 没生效)'); return false; }
await sleep(800);
}
// 详情:"Question Details" 下方输入框是 **TextView**(非 TextField,原代码填错框导致 0/2000 空、Submit 无效)
const det = await driver.findElementRaw('predicate string', 'type == "XCUIElementTypeTextView"').catch(() => null);
if (!det) { console.log('FB-FAIL: iOS 无详情 TextView'); return false; }
await driver.tapElement(det).catch(() => {});
await driver.typeText(det, details); await sleep(500);
for (const k of ['Done', 'Return', 'done', '完成']) { const kb = await driver.findElementRaw('name', k).catch(() => null); if (kb) { await driver.tapElement(kb); break; } }
} else {
// Android:开 picker → 重置到顶 → 上滑 typeIndex 格 → Confirm(坐标盲滑,选项无障碍不可见)
if (!(await tapText(driver, 'Please select'))) { await driver.tap(540, 460); }
await sleep(1200);
const COL_X = 540, MID = 2090, TOP = 1995;
for (let i = 0; i < 8; i++) { await driver.swipe(COL_X, TOP, COL_X, MID, 0.5); await sleep(250); }
await sleep(300);
for (let i = 0; i < typeIndex; i++) { await driver.swipe(COL_X, MID, COL_X, TOP, 0.5); await sleep(250); }
await sleep(300);
if (!(await tapText(driver, 'Confirm'))) { await driver.tap(947, 1714); }
await sleep(2000);
// 填 Question Details(末个 EditText)
const eds = await driver.findElementsRaw('-android uiautomator', 'new UiSelector().className("android.widget.EditText")');
console.log(`FB: 选完类型,EditText 数=${eds.length}`);
if (!eds.length) { console.log('FB-FAIL: 无 EditText(详情框)'); return false; }
await driver.typeText(eds[eds.length - 1], details);
await sleep(500);
try { await driver.goBack(); } catch { /* 收键盘 */ }
}
// Submit
if (!(await tapText(driver, 'Submit'))) { console.log('FB-FAIL: 无 Submit 按钮'); return false; }
console.log('FB: 已点 Submit,等 Ticket ID...');
await sleep(4000);
// 成功页:My Feedback / Ticket ID
const ok = await waitForSource(driver, 'Ticket ID', 8000).catch(() => false);
console.log(`FB: Ticket ID 出现=${!!ok}`);
return ok as boolean;
}
/**
* feedback 提交成功后,从结果页抓取工单号并持久化(供「获取RN插件 / 插件热更」用例消费)。
* 返回工单号;未抓到返回 null。
*/
export async function captureFeedbackTicketId(driver: DeviceDriver): Promise<string | null> {
let ticketId: string | null = null;
for (let i = 0; i < 3 && !ticketId; i++) {
const source = await driver.getSource();
ticketId = extractTicketId(source);
if (!ticketId) await sleep(1500);
}
if (ticketId) {
setSharedTicketId(ticketId);
console.log(`feedback 工单号已捕获并保存: ${ticketId}`);
} else {
console.log('feedback 提交页未解析到工单号(检查 extractTicketId 规则与实际 UI)');
}
return ticketId;
}