getB64.mjs

import get from 'lodash-es/get.js'
import isint from 'wsemi/src/isint.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import iser from 'wsemi/src/iser.mjs'
import cint from 'wsemi/src/cint.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import puppeteer from 'puppeteer'


/**
 * 瀏覽網址並用screenshot取得圖片base64資料
 *
 * @memberOf w-puppeteer-uitest
 * @param {String} url 輸入瀏覽網址,可為本機檔案絕對或相對位置
 * @param {Object} [opt={}] 輸入設定檔,預設為{}
 * @param {Boolean} [opt.headless=true] 輸入是否以無頭方式開啟網頁,預設為true
 * @param {Object} [opt.viewport={ width: 800, height: 600 }] 輸入網頁開啟後之viewport,預設為{ width: 800, height: 600 }
 * @param {Array} [opt.actions=[]] 輸入網頁開啟後之操作動作,預設為[]
 * @param {Object} opt.action 輸入action動作物件
 * @param {Object} opt.action.mode 動作模式字串,可選'wait','move','elemove','elehover',drag','eledrag','click','eleclick','dbclick','eledbclick','type','eletype'
 * @param {Object} opt.action 若action.mode使用'wait',需再輸入{time},單位為毫秒
 * @param {Object} opt.action 若action.mode使用'resize',需再輸入{width,height},為網頁可視區域(viewport)的長寬,單位為整數
 * @param {Object} opt.action 若action.mode使用'move',需再輸入{x1,y1},為相對網頁內容左上角位置,單位為px
 * @param {Object} opt.action 若action.mode使用'elemove',需再輸入{selector,nth(可選)},selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素
 * @param {Object} opt.action 若action.mode使用'elehover',需再輸入{selector},selector為css選擇器,若有超過1個結果則取第1個dom元素
 * @param {Object} opt.action 若action.mode使用'drag',需再輸入{x1,y1,x2,y2},由(x1,y1)拖曳至(x2,y2),為相對網頁內容左上角位置,單位為px
 * @param {Object} opt.action 若action.mode使用'eledrag',需再輸入{selector,nth(可選),shiftx,shifty},由元素中心拖曳平移(shiftx,shifty),selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素,shiftx,shifty單位為px
 * @param {Object} opt.action 若action.mode使用'click',需再輸入{x1,y1},為相對網頁內容左上角位置,單位為px
 * @param {Object} opt.action 若action.mode使用'eleclick',需再輸入{selector,nth(可選)},selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素
 * @param {Object} opt.action 若action.mode使用'dbclick',需再輸入{x1,y1},為相對網頁內容左上角位置,單位為px
 * @param {Object} opt.action 若action.mode使用'eledbclick',需再輸入{selector,nth(可選)},selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素
 * @param {Object} opt.action 若action.mode使用'type',需再輸入{str,noEnter(可選)},為由當前焦點輸入文字str,noEnter為輸入文字結尾不再輸入enter,預設為true
 * @param {Object} opt.action 若action.mode使用'eletype',需再輸入{selector,nth(可選),str,noEnter(可選)},selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素,通過click該dom元素作為焦點輸入文字str,noEnter為輸入文字結尾不再輸入enter,預設為true
 * @param {Object} opt.action 若action.mode使用'keypress',需再輸入{key,count(可選)},為由當前焦點輸入鍵盤key值,key可用'Backspace', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'或其他keycode,count為觸發次數,預設為1次,keypress可用key詳見[https://github.com/GoogleChrome/puppeteer/blob/v1.19.0/lib/USKeyboardLayout.js]
 * @param {Object} opt.action 若action.mode使用'elefocus',需再輸入{selector,nth(可選),str},selector為css選擇器,nth為陣列結果取第nth個dom元素,預設為0,負值代表由最末往前選(-nth)個dom元素,設定dom元素為當前焦點
 * keypress
 * @param {Integer} [opt.waitsec=5] 輸入開啟網頁後之等待時間,單位為秒,預設為5
 * @returns {String} 回傳screenshot圖片轉base64資料
 */
async function getB64(url, opt = {}) {


    //headless
    let headless = get(opt, 'headless')
    if (!isbol(headless)) {
        headless = true
    }


    //viewport
    let viewport = get(opt, 'viewport')
    if (!isobj(viewport)) {
        viewport = {
            x: 0,
            y: 0,
            width: 800,
            height: 600,
        }
    }


    //action
    let actions = get(opt, 'actions')
    if (!isarr(actions)) {
        actions = []
    }


    //waitsec
    let waitsec = get(opt, 'waitsec')
    if (!isint(waitsec)) {
        waitsec = 5
    }


    //new page
    let olaunch = {
        //timeout: 0,
        headless: headless,
        slowMo: 20,
        // arg: [
        //     //`--window-size=${viewport.width},${viewport.height}`, //無效
        //     //`--disable-resize-lock`, //無效
        // ],
        // defaultViewport: { //無效
        //     x: 0,
        //     y: 0,
        //     width: 800,
        //     height: 600,
        // }
    }
    let browser = await puppeteer.launch(olaunch)
    let page = await browser.newPage()


    //show page
    await page.goto(url)
    await page.setViewport(viewport)
    // await page.emulate({ //無效
    //     viewport: {
    //         width: viewport.width,
    //         height: viewport.height,
    //     },
    //     userAgent: ''
    // })

    //getxy
    async function getxy(selector, n = 0) {

        //n
        n = cint(n)

        //ele
        let ele
        if (n === 0) {
            ele = await page.$(selector)
            if (iser(ele)) {
                console.log('page.$ get no result: ' + selector)
                return null
            }
        }
        else {
            let eles = await page.$$(selector)
            if (eles.length === 0) {
                console.log('page.$$ get no result: ' + selector)
                return null
            }
            if (n < 0) {
                ele = eles[eles.length + n]
            }
            else {
                ele = eles[n]
            }
        }

        //check
        if (iser(ele)) {
            console.log('invalid ele: ' + selector)
            return null
        }

        //check
        if (iser(ele.boundingBox)) {
            console.log('invalid ele.boundingBox: ' + selector)
            return null
        }

        //r
        let r = await ele.boundingBox()
        r['cx'] = r.x + r.width / 2
        r['cy'] = r.y + r.height / 2
        r['ele'] = ele
        // let r = await page.evaluate((e) => {
        //     let { top, left, bottom, right } = e.getBoundingClientRect()
        //     return {
        //         top: top,
        //         left: left,
        //         bottom: bottom,
        //         right: right,
        //         cx: (left + right) / 2,
        //         cy: (top + bottom) / 2,
        //     }
        // }, ele)

        return r
    }


    //wait
    await page.waitFor(waitsec * 1000)


    //pmSeries
    await pmSeries(actions, async function(v) {
        if (v.mode === 'wait') {
            await page.waitFor(v.time)
        }
        else if (v.mode === 'resize') {
            await page.waitFor(300)
            await page.setViewport({
                x: 0,
                y: 0,
                width: v.width,
                height: v.height,
            })
            await page.waitFor(300)
        }
        else if (v.mode === 'move') {
            await page.waitFor(300)
            await page.mouse.move(v.x1, v.y1)
            await page.waitFor(300)
        }
        else if (v.mode === 'elemove') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await page.mouse.move(r.cx, r.cy)
                await page.waitFor(300)
            }
        }
        else if (v.mode === 'elehover') {
            await page.waitFor(300)
            await page.hover(v.selector)
            await page.waitFor(300)
        }
        else if (v.mode === 'drag') {
            await page.waitFor(300)
            await page.mouse.move(v.x1, v.y1)
            await page.mouse.down()
            await page.waitFor(300)
            await page.mouse.move(v.x2, v.y2, { steps: 50 })
            await page.waitFor(300)
            await page.mouse.up()
            await page.waitFor(300)
        }
        else if (v.mode === 'eledrag') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await page.mouse.move(r.cx, r.cy)
                await page.mouse.down()
                await page.waitFor(300)
                await page.mouse.move(r.cx + v.shiftx, r.cy + v.shifty, { steps: 50 })
                await page.waitFor(300)
                await page.mouse.up()
                await page.waitFor(300)
            }
        }
        else if (v.mode === 'click') {
            await page.waitFor(300)
            await page.mouse.move(v.x1, v.y1)
            await page.mouse.down()
            await page.mouse.up()
            await page.waitFor(300)
        }
        else if (v.mode === 'eleclick') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await r.ele.click()
                // await page.mouse.move(r.cx, r.cy)
                // await page.waitFor(50)
                // await page.mouse.down()
                // await page.mouse.up()
                await page.waitFor(300)
            }
        }
        else if (v.mode === 'dbclick') {
            await page.waitFor(300)
            await page.mouse.click(v.x1, v.y1, { clickCount: 2 })
            await page.waitFor(300)
        }
        else if (v.mode === 'eledbclick') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await r.ele.click({ clickCount: 2 })
                //await page.mouse.move(r.cx, r.cy)
                // //await page.waitFor(50)
                // await page.mouse.down()
                // await page.mouse.move(r.cx, r.cy)
                // await page.mouse.up()
                // //await page.waitFor(50)
                // await page.mouse.move(r.cx, r.cy)
                // await page.mouse.down()
                // await page.mouse.move(r.cx, r.cy)
                // await page.mouse.up()
                await page.waitFor(300)
            }
        }
        else if (v.mode === 'type') {
            await page.waitFor(300)
            await page.keyboard.type(v.str, { delay: 50 })
            if (!v.noEnter) { //預設為true
                await page.keyboard.type(String.fromCharCode(13))
            }
            await page.waitFor(300)
        }
        else if (v.mode === 'eletype') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await r.ele.click()
                await page.waitFor(50)
                await page.keyboard.type(v.str, { delay: 50 })
                if (!v.noEnter) { //預設為true
                    await page.keyboard.type(String.fromCharCode(13))
                }
                await page.waitFor(300)
            }
        }
        else if (v.mode === 'keypress') {
            await page.waitFor(300)
            let count = cint(v.count)
            if (count <= 0) {
                count = 1
            }
            for (let i = 0; i < count; i++) {
                await page.keyboard.press(v.key) //Backspace, ArrowLeft, ArrowRight, ArrowUp, ArrowDown
            }
            await page.waitFor(300)
        }
        else if (v.mode === 'elefocus') {
            await page.waitFor(300)
            let r = await getxy(v.selector, v.nth)
            if (r) {
                await r.ele.focus()
                await page.waitFor(300)
            }
        }
        else {
            console.log('mode is unrecognized: ' + v.mode)
        }
    })


    //wait
    await page.waitFor(2000) //預設等2s才視為action全部結束


    // //get base64 from screenshot, 此法雖能sceenshot到tooltip, 但內容物(例:ag-grid)會爆版
    // //add html2canvas and html2pic64
    // await page.addScriptTag({ url: 'https://html2canvas.hertzen.com/dist/html2canvas.min.js' })
    // let cfun = `
    // function html2pic64(ele) {
    //     //類似print screen將html轉為base64圖片

    //     return new Promise((resolve, reject) => {
    //         let opt={
    //             logging:false, //不要console.log
    //             scale:1, //anti-aliasing
    //             width:${viewport.width},
    //             height:${viewport.height},
    //         }
    //         html2canvas(ele, opt)
    //             .then(function (canvas) {
    //                 resolve(canvas.toDataURL('image/png'))
    //             })
    //             .catch(function (msg) {
    //                 reject(msg)
    //             })
    //     })
    // }
    // `
    // await page.addScriptTag({ content: cfun })
    // let base64 = await page.evaluate(() => {
    //     return html2pic64(document.body)
    // })
    // base64 = base64.replace('data:image/png;base64,', '') //取代掉給img用之開頭符號


    //get base64 from screenshot, 此法最不會爆版, 但screenshot前會先resize至原始dpi(例:96), 導致tooltip被自動隱藏無法截到圖
    //已恢復正常
    let base64 = await page.screenshot({ encoding: 'base64' }) //fullPage: true


    //close
    await page.close()
    await browser.close()


    return base64
}


export default getB64