waitUntilExist.mjs

/**
 * 偵測驅動之等待:每步驟先偵測對象存在或就緒再進下一步,逾時即拋錯(代表真實異常,而非固定等待不夠)
 *
 * fn於瀏覽器端執行,跨行程序列化故不可使用閉包變數,傳值一律經opt.arg。
 * fn為async函數(例如頁面內fetch後端再判斷)時改為逐次page.evaluate並await其結果輪詢:Playwright之waitForFunction同步呼叫predicate,
 * async函數回傳之Promise為truthy而第一次即放行(不重試、逾時無效,條件不成立也照樣通過;2026-09-28查得共20處:
 * w-web-sso之ag-grid等待以async判斷直呼waitForFunction 14處、w-web-task以async判斷呼叫waitUntilExist 6處)
 *
 * @param {Page} page 輸入Playwright之Page
 * @param {String} label 輸入錯誤訊息用之對象描述字串
 * @param {Function} fn 輸入於頁面執行之判斷函數,回傳truthy代表就緒;可為async函數
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Number} [opt.timeout=10000] 輸入逾時毫秒數,預設10000
 * @param {*} [opt.arg=null] 輸入傳給fn之參數,預設null
 * @param {Number} [opt.polling=100] 輸入async函數之輪詢間隔毫秒數,預設100
 * @returns {Promise} 回傳Promise,resolve代表就緒,逾時reject
 * @example
 *
 * import waitUntilExist from 'w-package-tools-e2e/src/waitUntilExist.mjs'
 *
 * await waitUntilExist(page, '表格列', () => document.querySelectorAll('.ag-row').length > 0)
 * await waitUntilExist(page, '訊息', (t) => document.body.innerText.includes(t), { arg: '儲存成功' })
 *
 */
async function waitUntilExist(page, label, fn, opt = {}) {
    let { timeout = 10000, arg = null, polling = 100 } = opt
    let fail = () => new Error(`waitUntilExist 超過 ${timeout}ms 仍找不到「${label}」 — 此為真實異常 (production race / 元件未渲染)`)
    if (typeof fn === 'function' && fn.constructor && fn.constructor.name === 'AsyncFunction') {
        let deadline = Date.now() + timeout
        while (Date.now() < deadline) {
            //evaluate會await fn回傳之Promise; 導頁中之評估失敗視為未就緒, 繼續輪詢
            let v = await page.evaluate(fn, arg).catch(() => false)
            if (v) {
                return
            }
            await new Promise((resolve) => setTimeout(resolve, polling))
        }
        throw fail()
    }
    try {
        await page.waitForFunction(fn, arg, { timeout })
    }
    catch (err) {
        throw fail()
    }
}


export default waitUntilExist