2.9 playwright之python实现

1、目录结构如下

2、main.py

import os
import shutil

from playwright.sync_api import sync_playwright
from config.setting import config
from utils.template import Template
from utils.md5 import Md5
from utils.delete import del_files
import pytest
from utils.dir_check import check_dir
from utils.baseurl import get_baseUrl


def run():
    check_dir()
    data = os.listdir('data')
    m = Md5('case', 'log', 'case_md5.json')
    n = Md5('utils', 'log', 'template_md5.json')
    filter_list = m.filter()
    utils_list = n.filter()
    if 'template.py' not in utils_list:
        filter_list = []
        n.write_md5()
    for i in data:
        file_path = 'data' + '/' + i
        if os.path.isfile(file_path):
            temp = 'test_' + i
            if temp not in filter_list:
                Template.create_test_file(file_path, 'case')
    m.write_md5()


if __name__ == "__main__":
    run()
    del_files('results')
    pytest.main(['-s', '--alluredir=results'])
    os.system('allure generate --clean ./results/ -o ./report/')
    for file_name in os.listdir('resource'):
        src_file = os.path.join('resource', file_name)
        dst_file = os.path.join('report', file_name)
        if os.path.exists(dst_file):
            os.remove(dst_file)
        shutil.copy(src_file, 'report')
    os.system('allure open -h 127.0.0.1 -p 8883 ./report/')

 3、conftest.py

import pytest
from playwright.sync_api import sync_playwright
from config.setting import config
from playwright.sync_api import Page
from utils.operate import operate
from utils.baseurl import get_baseUrl
import os
import allure
from utils.video import generate_video


@pytest.fixture(scope='session')
def page():
    browser = sync_playwright().start().chromium.launch(headless=False, slow_mo=500)
    page = browser.new_page(ignore_https_errors=True, record_video_dir='temp')
    page.goto(get_baseUrl(config))
    operate(config['username'], page)
    operate(config['password'], page)
    operate(config['submit'], page)
    return page


def log(request):
    with open('log/http.txt', 'a', encoding='utf-8') as w:
        w.write(f'{request}.url' + '\n')


@pytest.fixture(scope='function', autouse=True)
def after(page: Page):
    yield
    page.on("request", lambda request: log(request))


@pytest.fixture(scope='session', autouse=True)
def clear(page: Page):
    yield
    # page.close()
    p = generate_video('temp', 'video')
    allure.attach.file(p, f'{os.path.basename(p)}', attachment_type=allure.attachment_type.WEBM, extension='WEBM')

4、case目录,内容和目录都是自动生成

5、config目录,保存配置

dir_collection.py

配置中的目录都是自动生成

dir_collections = [
    'case',
    'log',
    'img',
    'video',
    'temp'
]

env.py

环境变量配置

env = {
    'prod': '',
    'dev': '',
    'test': 'http://test.lan'
}

setting.py

config = {
    'baseUrl': '',
    'url': '/login',
    'username': {
        'selector': '#userName',
        'type': 'input',
        'value': 'test'
    },
    'password': {
        'selector': '#password',
        'type': 'input',
        'value': '123'
    },
    'submit': {
        'selector': '#root > div > div > div:nth-child(1) > div > form > div:nth-child(3) > button',
        'type': 'button'
    },
}

6、data目录

case中的测试文件,便是依据data中的数据自动生成的

home.py

home_cfg = [
    {
        'name': 'home',
        'url': '',
        'step': [
        ],
        'assert': [
            {
                'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(1) > div '
                            '> div > div._3A9TZ-vnPrcf2IwqBmUPoX',
                'value': '违规告警数量'
            },
            {
                'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(2) > div '
                            '> div > div._3A9TZ-vnPrcf2IwqBmUPoX',
                'value': '确认违规告警数量1'
            }
 
        ]
    }

]

key.py

key_cfg = [
    {
        'name': 'key',
        'url': '/key/info',
        'step': [
            {
                "type": 'input',
                "selector": 'text=名称',
                "value": 'UI测试'
            },
            {
                "type": 'input',
                "selector": 'text=描述',
                "value": 'UI新建关键词'
            },
        ],
        'assert': [
            {
                'selector': '#content > div > div > div > h3',
                'value': '新增'
            },
            {
                'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(1) > div.ant-form-item-label > label',
                'value': 'test名称'
            },
            {
                'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(2) > div.ant-form-item-label > label',
                'value': 'test描述'
            },
        ]
    }

]

7、img目录,保存错误截图的目录,自动生成

8、log目录,保存请求日志和两个md5文件,这两个md5文件主要用来辨别每次运行是否要重新生成case目录中的测试文件

9、report目录,allure命令自动生成

10、resource目录,由于对allure的报告进行了小量的修改,所以,需要保留resource目录,当生成report后,就会将resource目录中的内容和report中的内容替换

11、results目录,allure命令生成,保存测试结果数据

12、temp目录,自动生成临时目录,录制的视频文件会存到temp,然后会对视频进行改名另存到video目录,temp每次运行前自动生成,运行后,自动删除

13、utils目录,存储封装方法的目录

add_style.py

from playwright.sync_api import Page


def add_style(page: Page, elements, flag: int):
    if flag == 0:
        script = f"document.querySelector('{elements}').setAttribute('style','border-style:solid " \
                 f";border-color:green') "
    else:
        script = f"document.querySelector('{elements}').setAttribute('style','border-style:solid " \
                 f";border-color:red') "
    page.evaluate(script)

assert_element.py

from typing import List
from playwright.sync_api import Page
from utils.add_style import add_style
from utils.screenshot import error_screenshot


def assert_element(arr: List, page: Page):
    li = []
    if arr:
        for i in arr:
            if page.query_selector(i['selector']):
                text = page.query_selector(i['selector']).inner_text()
                if text == i['value']:
                    add_style(page, i['selector'], 0)
                    pass
                else:
                    add_style(page, i['selector'], 1)
                    li.append(i['value'])
            else:
                li.append(i['selector'])
    if li:
        error_screenshot(page, 'img')
        raise AssertionError(f"some elements in {str(li)} isn't matched or exists")

baseurl.py

from typing import Dict
from config.env import env
from utils.params_error import ParamsError


def get_baseUrl(conf: Dict):
    import sys
    if len(sys.argv) > 1:
        if sys.argv[1] == 'dev':
            conf['baseUrl'] = env['dev']
        elif sys.argv[1] == 'prod':
            conf['baseUrl'] = env['prod']
        elif sys.argv[1] == 'test':
            conf['baseUrl'] = env['test']
        else:
            raise ParamsError('python main.py [test]|[prod]|[dev]')
    else:
        raise ParamsError('python main.py [test]|[prod]|[dev]')
    url = conf['baseUrl'] + conf['url']
    return url

delete.py

import os


def del_files(dir_path: str):
    if os.path.exists(dir_path):
        for filename in os.listdir(dir_path):
            filepath = os.path.join(dir_path, filename)
            try:
                if os.path.isfile(filepath):
                    os.unlink(filepath)
            except Exception as e:
                print(f"Error deleting {filepath}: {e}")

dir_check.py

import os
from config.dir_collection import dir_collections


def check_dir():
    li = os.listdir()
    for i in dir_collections:
        if i not in li:
            os.mkdir(i)

md5.py

import hashlib
import json
import os
from json import JSONDecodeError


class Md5:
    def __init__(self, dir_path, md5_path, file_name):
        self.dir_path = dir_path  # 目录路径
        self.md5_path = md5_path  # MD5文件路径
        self.file_name = file_name  # MD5文件名
        file_path = os.path.join(md5_path, file_name)
        if not os.path.exists(file_path):
            open(file_path, mode='w+', encoding='utf-8').close()  # 如果MD5文件不存在,则创建该文件

    def generate_md5(self):
        temp = {}
        # 如果dir_path是文件而不是目录,则抛出IOError异常
        if os.path.isfile(self.dir_path):
            raise IOError(f'Message: parameter <dir_path:{self.dir_path}> must be directory')
        else:
            dir_list = os.listdir(self.dir_path)  # 获取目录下的文件列表
            if len(dir_list) != 0:
                for i in dir_list:
                    md5 = hashlib.md5()  # 创建MD5对象
                    file_path = os.path.join(self.dir_path, i)  # 获取文件路径
                    if os.path.isfile(file_path) and os.path.basename(file_path).endswith('.py'):  # 如果是文件
                        with open(file_path, mode='r', encoding='utf-8') as f:
                            md5.update(f.read().encode(encoding='utf-8'))  # 更新MD5值
                            hex_md5 = md5.hexdigest()  # 获取MD5值
                            temp[i] = hex_md5  # 将文件名和MD5值添加到字典中
            return temp  # 返回字典

    def write_md5(self):
        file_path = os.path.join(self.md5_path, self.file_name)
        # 将generate_md5()生成的字典写入到文件中
        json.dump(self.generate_md5(), open(file_path, mode='w+', encoding='utf-8'))

    def read_md5(self):
        file_path = os.path.join(self.md5_path, self.file_name)
        try:
            with open(file_path, mode='r', encoding='utf-8') as f:
                # 读取文件中的json数据并返回
                return json.load(f)
        except JSONDecodeError:
            # 如果文件中的json数据解析失败,则返回空字典
            return {}

    def filter(self):
        old_md5 = self.read_md5()  # 获取旧的MD5值
        new_md5 = self.generate_md5()  # 获取新的MD5值
        # 返回新旧md5值相同的文件名列表
        return [k for k, v in new_md5.items() if k in old_md5 and v == old_md5[k]]

operate.py

from playwright.sync_api import Page


def operate(d: dict, page: Page):
    if d.get('type') == 'input':
        page.query_selector(d.get('selector')).fill(d.get('value'))
    elif d.get('type') == 'button':
        page.query_selector(d.get('selector')).click()

params_error.py

class ParamsError(Exception):
    def __init__(self, msg: str):
        super(ParamsError, self).__init__(msg)

parse.py

from playwright.sync_api import Page
from config.setting import config


def parse(conf: dict, page: Page):
    url = config['baseUrl'] + conf['url']
    if url != '':
        page.goto(url)
    if conf['step']:
        for i in conf['step']:
            if i.get('type') == 'input':
                page.query_selector(i.get('selector')).fill(i.get('value'))
            elif i.get('type') == 'button':
                page.query_selector(i.get('selector')).click()

screenshot.py

import time

import allure
from playwright.sync_api import Page


def error_screenshot(page: Page, path: str):
    file_path = f'{path}/{int(time.time())}.png'
    page.screenshot(path=file_path, type='png', full_page=True)
    allure.attach.file(file_path, f'{path}/{int(time.time())}', attachment_type=allure.attachment_type.PNG,
                       extension='PNG')

template.py

import os


class Template:
    @staticmethod
    def check_todo_file(file_path: str) -> bool:
        """
        检查文件内容中是否包含 '# TODO' 字符串

        Args:
            file_path (str): 文件路径

        Returns:
            bool: 如果包含 '# TODO' 字符串则返回 True,否则返回 False
        """
        with open(file_path, mode='r+', encoding='utf-8') as file:
            return '# TODO' in file.read()

    @staticmethod
    def create_test_file(file_path: str, target_path: str) -> None:
        """
        创建测试文件

        Args:
            file_path (str): 文件路径
            target_path (str): 目标路径
        """
        if Template.check_todo_file(file_path):
            print(f'Message: 发现 <TODO> 标记,文件 <{file_path}> 尚未完成')
            return

        file_name = os.path.basename(file_path).replace('.py', '')
        import_name = f'{file_name}_cfg'
        test_file_path = os.path.join(target_path, f'test_{file_name}.py')

        with open(test_file_path, mode='w+', encoding='utf-8') as file:
            file.write(f'''import pytest
import allure
from data.{file_name} import {import_name}
from playwright.sync_api import Page
from utils.parse import parse
from utils.assert_element import assert_element


@allure.suite('{file_name}')
class Test_{file_name.capitalize()}:

    @allure.sub_suite('{import_name}')
    @pytest.mark.parametrize('cfg', {import_name})
    def test_{file_name}(self, cfg, page):  
        parse(cfg, page)
        allure.dynamic.title(cfg['name'])
        assert_element(cfg['assert'], page)
''')
        print(f"Message: 文件 <{test_file_path}> 创建成功")

 video.py

import os
import time


# def remove_video(path: str):
#     print(os.listdir(path))
#     if os.listdir(path):
#         for i in os.listdir(path):
#             os.remove(f'{path}/{i}')


def generate_video(source_path: str, target_path: str):
    p = f"{target_path}/{int(time.time())}.webm"
    while True:
        if os.listdir(source_path):
            for i in os.listdir(source_path):
                os.renames(f'{source_path}/{i}', p)
            break
    return p

14、video目录自动生成,存放录制视频的目录

15、报告展示

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值