2.9 python implementation of playwright

1. The directory structure is as follows

2. main.py

import os
import shut-off

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' + '\\
')


@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 directory, content and directory are automatically generated

5. config directory, save the configuration

dir_collection.py

The directories in the configuration are automatically generated

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

env.py

Environment variable configuration

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

setting.py

config = {
    'baseUrl': '',
    'url': '/user/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 directory

The test file in the case is automatically generated based on the data in the data

homepage.py

homepage_cfg = [
    {
        'name': 'homepage',
        'url': '',
        'step': [
        ],
        'assert': [
            {
                'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(1) > div '
                            '> div > div._3A9TZ-vnPrcf2IwqBmUPoX',
                'value': 'Number of violation warnings'
            },
            {
                'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(2) > div '
                            '> div > div._3A9TZ-vnPrcf2IwqBmUPoX',
                'value': 'Confirm the number of violation alarms 1'
            },
            {
                'selector': '#content > div > div > div > div > div.react-grid-layout.layout > div:nth-child(3) > div '
                            '> div > div._3A9TZ-vnPrcf2IwqBmUPoX',
                'value': 'Number of unconfirmed violation alerts'
            },
        ]
    }

]

keyword.py

keyword_cfg = [
    {
        'name': 'keyword',
        'url': '/keyword/info',
        'step': [
            {
                "type": 'input',
                "selector": 'text=keyword group name',
                "value": 'UI Test'
            },
            {
                "type": 'input',
                "selector": 'text=keyword group description',
                "value": 'UI new keywords'
            },
        ],
        'assert': [
            {
                'selector': '#content > div > div > div > h3',
                'value': 'New keyword strategy'
            },
            {
                'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(1) > div.ant-form-item-label > label',
                'value': 'keyword group name'
            },
            {
                'selector': '#content > div > div > div > div > div > div > div > form > div:nth-child(2) > div.ant-form-item-label > label',
                'value': 'keyword phrase description'
            },
        ]
    }

]

7. img directory, the directory for saving error screenshots, automatically generated

8, log directory, save the request log and two md5 files, these two md5 files are mainly used to identify whether to regenerate the test files in the case directory for each run

9. Report directory, automatically generated by allure command

10. The resource directory, because a small amount of modification has been made to the report of allure, so the resource directory needs to be kept. When the report is generated, the contents in the resource directory and the contents in the report will be replaced

11. The results directory, generated by the allure command, saves the test result data

12. The temp directory will automatically generate a temporary directory. The recorded video files will be saved to temp, and then the video will be renamed and saved to the video directory. Temp will be automatically generated before each run, and will be automatically deleted after running

13, utils directory, the directory for storing encapsulation methods

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 # directory path
        self.md5_path = md5_path # MD5 file path
        self.file_name = file_name # MD5 file name
        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() # If the MD5 file does not exist, create the file

    def generate_md5(self):
        temp = {}
        # If dir_path is a file instead of a directory, throw an IOError exception
        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) # Get the list of files in the directory
            if len(dir_list) != 0:
                for i in dir_list:
                    md5 = hashlib.md5() # create MD5 object
                    file_path = os.path.join(self.dir_path, i) # get file path
                    if os.path.isfile(file_path) and os.path.basename(file_path).endswith('.py'): # If it is a file
                        with open(file_path, mode='r', encoding='utf-8') as f:
                            md5.update(f.read().encode(encoding='utf-8')) # update MD5 value
                            hex_md5 = md5.hexdigest() # Get MD5 value
                            temp[i] = hex_md5 # add the filename and MD5 value to the dictionary
            return temp # return dictionary

    def write_md5(self):
        file_path = os.path.join(self.md5_path, self.file_name)
        # Write the dictionary generated by generate_md5() to the file
        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:
                # Read the json data in the file and return
                return json. load(f)
        except JSONDecodeError:
            # If the json data in the file fails to parse, return an empty dictionary
            return {}

    def filter(self):
        old_md5 = self.read_md5() # Get the old MD5 value
        new_md5 = self.generate_md5() # Get new MD5 value
        # Return a list of filenames with the same old and new md5 values
        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:
        """
        Check if the file content contains '# TODO' string

        Args:
            file_path (str): file path

        Returns:
            bool: Returns True if contains '# TODO' string, otherwise returns 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:
        """
        Create test file

        Args:
            file_path (str): file path
            target_path (str): target path
        """
        if Template.check_todo_file(file_path):
            print(f'Message: <TODO> tag found, file <{file_path}> not yet completed')
            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: File <{test_file_path}> created successfully")

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. The video directory is automatically generated to store the recorded video directory

15. Reporting effects

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledgePython entry skill treeHomepageOverview 298962 people are studying systematically