150 lines
9.4 KiB
TypeScript
150 lines
9.4 KiB
TypeScript
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;
|
||
}
|