基于pyppeteer实现迷你Web测试框架

简介:

Puppeteer(中文翻译”木偶”) 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具,它是一个 Node 库,提供了一个高级的 API 来控制 DevTools协议上的无头版 Chrome 。也可以配置为使用完整(非无头)的 Chrome。Chrome 素来在浏览器界稳执牛耳,因此,Chrome Headless 必将成为 web 应用自动化测试的行业标杆。使用 Puppeteer,相当于同时具有 Linux 和 Chrome 双端的操作能力,应用场景可谓非常之多。此仓库的建立,即是尝试各种折腾使用 GoogleChrome Puppeteer;以期在好玩的同时,学到更多有意思的操作。

Pyppeteer是基于Puppeteer协议,用python实现的异步框架。本人在此基础上,做了简易封装。

核心代码:

ExtendPageOrFrame类定义了更方便的方法:

class ExtendPageOrFrame(pyppeteer.page.Page):
    def __init__(self, page_frame_obj):
        self.page = page_frame_obj
        self.all_pending_requests = []
        self.all_finished_requests = []

    def __getattr__(self, name):
        if name not in ['page', 'all_pending_requests', 'all_finished_requests',
                        'click', 'set_value', 'set_value',
                        'set_el_radio', 'get_el_radio', 'waitForAllXhrFinished']:
            return getattr(self.page, name)
        else:
            return getattr(self, name)

    async def click(self, selector_or_xpath, idx=0, wait_after=1):
        if selector_or_xpath is None:
            return
        elif selector_or_xpath.startswith('/'):
            elem = await self.page.xpath(selector_or_xpath)
            await elem[idx].click()
        else:
            await self.page.click(selector_or_xpath)
        await self.page.waitFor(wait_after * 1000)

    async def get_value(self, selector_or_xpath, idx=0):
        if selector_or_xpath is None:
            return
        else:
            if selector_or_xpath.startswith('/'):
                elements = await self.page.xpath(selector_or_xpath)
                element = elements[idx]
            else:
                element = await self.page.querySelector(selector_or_xpath)

            tag_name = await self.page.evaluate('(element) => element.tagName', element)
            # print(tag_name)
            if tag_name == 'INPUT':
                ele_type = await self.page.evaluate('(element) => element.getAttribute("type")', element)
                if ele_type == 'checkbox':
                    # print(ele_type)
                    value = await self.page.evaluate('(element) => element.checked', element)
                else:
                    value = await self.page.evaluate('(element) => element.value', element)
            elif tag_name == 'SELECT':
                value = await self.page.evaluate('''(element) => {
                var index=element.selectedIndex;
                return element.options[index].text;
                }''', element)
            else:
                value = await self.page.evaluate('(element) => element.textContent', element)
            return value

    async def set_value(self, selector_or_xpath, value, idx=0):
        if selector_or_xpath is None:
            return
        else:
            if selector_or_xpath.startswith('/'):
                elements = await self.page.xpath(selector_or_xpath)
                element = elements[idx]
            else:
                element = await self.page.querySelector(selector_or_xpath)

            tag_name = await self.page.evaluate('(element) => element.tagName', element)

            if tag_name == 'INPUT':
                ele_type = await self.page.evaluate('(element) => element.getAttribute("type")', element)
                if ele_type == 'checkbox':
                    if value is True:
                        await self.page.evaluate('(element) => {element.checked=true;}', element)
                    else:
                        await self.page.evaluate('(element) => {element.checked=false;}', element)
                else:
                    await element.type(value)
            elif tag_name == 'SELECT':
                await self.page.evaluate('''(element, value) => {
                    for(var i=0; i<element.options.length; i++){  
                            if(element.options[i].innerHTML == value){  
                                element.options[i].selected = true;  
                                break;  
                            }  
                        }
                    }''', element, value)
            else:
                pass

    async def set_el_radio(self, text, idx=0, wait_after=1):
        radios = await self.page.xpath(
            '//label[contains(@class,"el-radio")]/span[@class="el-radio__label" and contains(text(), "{}")]'.format(
                text))
        await radios[idx].click()
        await self.page.waitFor(wait_after * 1000)

    async def get_el_radio(self, idx=0):
        value = await self.get_value(
            '//label[contains(@class,"el-radio") and contains(@class,"is-checked")]/span[@class="el-radio__label"]',
            idx)
        return value.strip()

    async def wait_for_all_xhr_finished(self):
        def request_handler(r):
            if r.resourceType == 'xhr':
                self.all_pending_requests.append(r)

        def response_handler(fr):
            if fr.resourceType == 'xhr':
                self.all_finished_requests.append(fr)

        self.page.on('request', request_handler)
        self.page.on('requestfinished', response_handler)

        last_several_cnt = []
        loop_times = 0
        max_tries = 6

        while 1:
            loop_times += 1
            if len(last_several_cnt) == max_tries:
                last_several_cnt.pop(0)
            cur_num = len(self.all_pending_requests)
            last_several_cnt.append(cur_num)
            if len(set(last_several_cnt)) == 1 and loop_times >= max_tries:
                break
            await asyncio.sleep(0.1)
        # print('last 10 requests number:', last_several_cnt)
        while 1:
            if len(self.all_pending_requests) == len(self.all_finished_requests):
                break
            else:
                await asyncio.sleep(0.1)
        self.all_pending_requests = []
        self.all_finished_requests = []
        self.remove_listener('request', request_handler)
        self.remove_listener('requestfinished', response_handler)

BaseWebPage类启动浏览器、初始化

class BaseWebPage(object):
    def __init__(self, start_url, headless=False):
        self.url = start_url
        self.headless = headless

    async def navigate(self):
        browser = await pyppeteer.launch(headless=self.headless, args=['--start-maximized'])
        page = (await browser.pages())[0]

        page = ExtendPageOrFrame(page)
        current_screen = await page.evaluate('''() => {
        return {
            width: window.screen.availWidth,
            height: window.screen.availHeight,
            };
        }''')
        await page.setViewport(current_screen)

        await page.goto(self.url)
        await page.wait_for_all_xhr_finished()
        return page, browser

测试代码(使用上述自定义类):

async def test_async():
    page, browser = await BaseWebPage('http://ip:port/static/html/login.html').navigate()

    await page.set_value('input[name=username]', '')
    print(await page.get_value('input[name=username]'))
    await page.type('input[name=password]', '')
    await page.click('button[type=submit]', wait_after=3)

    # await page.waitForNavigation({'waitUntil': 'networkidle2'})

    await page.click('//span[text()=\'策略中心\']')
    await page.click('//a[text()=\'优先拣选策略\']', wait_after=3)

    iframe = await page.xpath('//iframe[@class="iframe"]')
    frame = await iframe[0].contentFrame()

    frame = ExtendPageOrFrame(frame)

    await frame.click('//table[@class="el-table__body"]/tbody/tr[1]/td[12]/div/button/span[contains(text(),"编辑")]')

    await frame.set_el_radio('每天')

    print(await frame.get_el_radio())

    await asyncio.sleep(2)

    await browser.close()


def test():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(test_async())


if __name__ == '__main__':
    test()
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容