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 { // 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 { // 进 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 { 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 { 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; }