playwright-go实战:自动化登录测试

news2025/4/1 6:03:03

1.新建项目

打开Goland新建项目playwright-go-demo

项目初始化完成后打开终端输入命令:

#安装项目依赖
go get -u github.com/playwright-community/playwright-go
#安装浏览器
go run github.com/playwright-community/playwright-go/cmd/playwright@latest install --with-deps

2.编写代码

项目结构

部分代码

config.go

package config

import (
    "encoding/json"
    "fmt"
    "os"
    "path/filepath"
)

// BrowserConfig 浏览器配置
type BrowserConfig struct {
    Type      string `json:"type"`      // 浏览器类型:chromium, firefox, webkit
    Headless  bool   `json:"headless"`  // 是否无头模式
    SlowMo    int    `json:"slowMo"`    // 慢动作模式,毫秒
    Maximized bool   `json:"maximized"` // 是否最大化
}

// LoginConfig 登录配置
type LoginConfig struct {
    Username        string `json:"username"`         // 用户名
    Password        string `json:"password"`         // 密码
    URL             string `json:"url"`              // 登录URL
    InvalidUsername string `json:"invalid_username"` // 无效用户名
    InvalidPassword string `json:"invalid_password"` // 无效密码
}

// Config 应用配置
type Config struct {
    Browsers []BrowserConfig `json:"browsers"` // 多浏览器配置
    Login    LoginConfig     `json:"login"`    // 登录配置
}

// DefaultConfig 默认配置
var DefaultConfig = Config{
    Browsers: []BrowserConfig{
        {
            Type:      "chromium",
            Headless:  false,
            SlowMo:    0,
            Maximized: true,
        },
        {
            Type:      "webkit",
            Headless:  false,
            SlowMo:    0,
            Maximized: true,
        },
    },
    Login: LoginConfig{
        Username:        "tomsmith",
        Password:        "SuperSecretPassword!",
        URL:             "http://the-internet.herokuapp.com/login",
        InvalidUsername: "invaliduser",
        InvalidPassword: "invalidpass",
    },
}

// LoadConfig 从文件加载配置
func LoadConfig(configPath string) (*Config, error) {
    // 如果配置文件不存在,创建默认配置文件
    if _, err := os.Stat(configPath); os.IsNotExist(err) {
        // 确保目录存在
        dir := filepath.Dir(configPath)
        if err := os.MkdirAll(dir, 0755); err != nil {
            return nil, fmt.Errorf("无法创建配置目录: %w", err)
        }

        // 写入默认配置
        file, err := os.Create(configPath)
        if err != nil {
            return nil, fmt.Errorf("无法创建配置文件: %w", err)
        }
        defer file.Close()

        encoder := json.NewEncoder(file)
        encoder.SetIndent("", "  ")
        if err := encoder.Encode(DefaultConfig); err != nil {
            return nil, fmt.Errorf("无法写入默认配置: %w", err)
        }

        return &DefaultConfig, nil
    }

    // 读取配置文件
    file, err := os.Open(configPath)
    if err != nil {
        return nil, fmt.Errorf("无法打开配置文件: %w", err)
    }
    defer file.Close()

    config := &Config{}
    if err := json.NewDecoder(file).Decode(config); err != nil {
        return nil, fmt.Errorf("无法解析配置文件: %w", err)
    }

    return config, nil
}

config.json

{
  "browsers": [
    {
      "type": "chromium",
      "headless": true,
      "slowMo": 0,
      "maximized": true
    },
    {
      "type": "webkit",
      "headless": true,
      "slowMo": 0,
      "maximized": true
    }
  ],
  "login": {
    "username": "tomsmith",
    "password": "SuperSecretPassword!",
    "url": "http://the-internet.herokuapp.com/login",
    "invalid_username": "invaliduser",
    "invalid_password": "invalidpass"
  }
}

pages/login_page.go

package pages

import (
    "fmt"
    "github.com/playwright-community/playwright-go"
)

// LoginPage 表示登录页面对象
type LoginPage struct {
    page     playwright.Page
    loginURL string
}

// NewLoginPage 创建一个新的登录页面对象
func NewLoginPage(page playwright.Page) *LoginPage {
    return &LoginPage{
        page:     page,
        loginURL: "http://the-internet.herokuapp.com/login", // 默认URL,将被配置文件中的URL覆盖
    }
}

// SetLoginURL 设置登录URL
func (l *LoginPage) SetLoginURL(url string) {
    l.loginURL = url
}

// Navigate 导航到登录页面
func (l *LoginPage) Navigate() error {
    _, err := l.page.Goto(l.loginURL, playwright.PageGotoOptions{
        WaitUntil: playwright.WaitUntilStateNetworkidle,
    })
    return err
}

// Login 执行登录操作
func (l *LoginPage) Login(username, password string) error {
    // 输入用户名
    if err := l.page.Fill("#username", username); err != nil {
        return fmt.Errorf("无法输入用户名: %w", err)
    }

    // 输入密码
    if err := l.page.Fill("#password", password); err != nil {
        return fmt.Errorf("无法输入密码: %w", err)
    }

    // 点击登录按钮
    if err := l.page.Click("button[type=\"submit\"]"); err != nil {
        return fmt.Errorf("无法点击登录按钮: %w", err)
    }

    // 等待页面加载完成
    if err := l.page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
        State: playwright.LoadStateNetworkidle,
    }); err != nil {
        return fmt.Errorf("等待页面加载超时: %w", err)
    }
    return nil
}

// VerifyLoginSuccess 验证登录是否成功
func (l *LoginPage) VerifyLoginSuccess() (bool, error) {
    // 等待成功消息出现
    successLocator := l.page.Locator(".flash.success")
    if err := successLocator.WaitFor(playwright.LocatorWaitForOptions{
        Timeout: playwright.Float(5000),
    }); err != nil {
        return false, fmt.Errorf("未找到成功消息: %w", err)
    }

    // 检查是否存在登出按钮
    logoutButton, err := l.page.IsVisible("a[href=\"/logout\"]")
    if err != nil {
        return false, fmt.Errorf("检查登出按钮失败: %w", err)
    }
    return logoutButton, nil
}

// Logout 执行登出操作
func (l *LoginPage) Logout() error {
    // 点击登出按钮
    if err := l.page.Click("a[href=\"/logout\"]"); err != nil {
        return fmt.Errorf("无法点击登出按钮: %w", err)
    }

    // 等待页面加载完成
    if err := l.page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
        State: playwright.LoadStateNetworkidle,
    }); err != nil {
        return fmt.Errorf("等待页面加载超时: %w", err)
    }
    return nil
}

// WaitForTimeout 等待指定时间
func (l *LoginPage) WaitForTimeout(ms int) {
    l.page.WaitForTimeout(float64(ms))
}

// VerifyLoginFailed 验证登录失败场景
func (l *LoginPage) VerifyLoginFailed() (bool, error) {
    // 等待错误消息出现
    errorLocator := l.page.Locator(".flash.error")
    if err := errorLocator.WaitFor(playwright.LocatorWaitForOptions{
        Timeout: playwright.Float(5000),
    }); err != nil {
        return false, fmt.Errorf("未找到错误消息: %w", err)
    }

    // 检查是否仍在登录页面(通过登录按钮是否可见来判断)
    loginButton, err := l.page.IsVisible("button[type=\"submit\"]")
    if err != nil {
        return false, fmt.Errorf("检查登录按钮失败: %w", err)
    }
    return loginButton, nil
}

main.go

package main

import (
    "fmt"
    "log"
    "os"
    "path/filepath"
    "time"

    "github.com/playwright-community/playwright-go"
    "github.com/wan/playwright-go-demo/config"
    "github.com/wan/playwright-go-demo/pages"
    "github.com/wan/playwright-go-demo/utils"
)

func main() {
    // 清理旧的测试结果
    if err := utils.CleanupOldTestResults(); err != nil {
        log.Printf("警告: 清理旧测试结果失败: %v", err)
    }

    // 加载配置文件
    configPath := "./config/config.json"
    cfg, err := config.LoadConfig(configPath)
    if err != nil {
        log.Fatalf("加载配置文件失败: %v", err)
    }

    // 初始化Playwright
    pw, err := playwright.Run()
    if err != nil {
        log.Fatalf("无法启动Playwright: %v", err)
    }
    defer pw.Stop()

    // 确保截图目录存在
    screenshotDir := "./screenshots"
    if _, err := os.Stat(screenshotDir); os.IsNotExist(err) {
        os.MkdirAll(screenshotDir, 0755)
    }

    // 确保视频目录存在
    videoDir := "./videos"
    if _, err := os.Stat(videoDir); os.IsNotExist(err) {
        os.MkdirAll(videoDir, 0755)
    }

    // 遍历所有配置的浏览器,分别执行测试
    for _, browserConfig := range cfg.Browsers {
        // 为每个浏览器创建单独的测试报告
        reportManager := utils.NewReportManager(fmt.Sprintf("%s浏览器登录测试", browserConfig.Type))

        // 执行特定浏览器的测试
        runTestWithBrowser(pw, browserConfig, cfg.Login, screenshotDir, videoDir, reportManager)
    }
}

// runTestWithBrowser 使用特定浏览器执行测试
func runTestWithBrowser(pw *playwright.Playwright, browserConfig config.BrowserConfig, loginConfig config.LoginConfig, screenshotDir, videoDir string, reportManager *utils.ReportManager) {
    // 根据配置选择浏览器类型
    var browserType playwright.BrowserType
    switch browserConfig.Type {
    case "firefox":
        browserType = pw.Firefox
    case "webkit":
        browserType = pw.WebKit
    default:
        browserType = pw.Chromium
    }

    fmt.Printf("开始使用 %s 浏览器执行测试\n", browserConfig.Type)

    // 创建浏览器实例
    browser, err := browserType.Launch(playwright.BrowserTypeLaunchOptions{
        Headless: playwright.Bool(browserConfig.Headless),
        SlowMo:   playwright.Float(float64(browserConfig.SlowMo)),
    })
    if err != nil {
        log.Printf("无法启动 %s 浏览器: %v", browserConfig.Type, err)
        return
    }
    defer browser.Close()

    // 创建上下文
    contextOptions := playwright.BrowserNewContextOptions{
        RecordVideo: &playwright.RecordVideo{
            Dir: filepath.Join(videoDir, browserConfig.Type), // 为每个浏览器创建单独的视频目录
        },
    }

    // 如果配置了最大化,设置视口大小为最大
    if browserConfig.Maximized {
        // 设置一个足够大的视口大小来模拟最大化
        contextOptions.Viewport = &playwright.Size{
            Width:  1920,
            Height: 1080,
        }
    }

    // 确保浏览器特定的视频目录存在
    browserVideoDir := filepath.Join(videoDir, browserConfig.Type)
    if _, err := os.Stat(browserVideoDir); os.IsNotExist(err) {
        os.MkdirAll(browserVideoDir, 0755)
    }

    // 确保浏览器特定的截图目录存在
    browserScreenshotDir := filepath.Join(screenshotDir, browserConfig.Type)
    if _, err := os.Stat(browserScreenshotDir); os.IsNotExist(err) {
        os.MkdirAll(browserScreenshotDir, 0755)
    }

    context, err := browser.NewContext(contextOptions)
    if err != nil {
        log.Printf("无法创建 %s 浏览器上下文: %v", browserConfig.Type, err)
        return
    }
    defer context.Close()

    // 创建页面
    page, err := context.NewPage()
    if err != nil {
        log.Printf("无法创建 %s 浏览器页面: %v", browserConfig.Type, err)
        return
    }
    reportManager.StartTest("登录测试")

    // 执行测试
    testStart := time.Now()

    try := func() bool {
        // 创建登录页面对象
        loginPage := pages.NewLoginPage(page)
        // 设置登录URL
        loginPage.SetLoginURL(loginConfig.URL)

        // 步骤1: 导航到登录页面
        reportManager.StartStep("导航到登录页面")
        if err := loginPage.Navigate(); err != nil {
            // 失败时截图
            screenshotPath := filepath.Join(browserScreenshotDir, "navigate_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("导航到登录页面失败", err, screenshotPath)
            return false
        }
        reportManager.EndStepSuccess("成功导航到登录页面")

        // 测试场景1: 使用错误的用户名登录
        reportManager.StartStep("测试错误用户名登录")
        if err := loginPage.Login("wrong_username", loginConfig.Password); err != nil {
            screenshotPath := filepath.Join(browserScreenshotDir, "wrong_username_input_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("输入错误用户名失败", err, screenshotPath)
            return false
        }
        if failed, err := loginPage.VerifyLoginFailed(); err != nil || !failed {
            screenshotPath := filepath.Join(browserScreenshotDir, "wrong_username_verify_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("验证错误用户名失败场景失败", err, screenshotPath)
            return false
        }
        reportManager.EndStepSuccess("成功验证错误用户名登录失败场景")

        // 测试场景2: 使用错误的密码登录
        reportManager.StartStep("测试错误密码登录")
        if err := loginPage.Login(loginConfig.Username, "wrong_password"); err != nil {
            screenshotPath := filepath.Join(browserScreenshotDir, "wrong_password_input_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("输入错误密码失败", err, screenshotPath)
            return false
        }
        if failed, err := loginPage.VerifyLoginFailed(); err != nil || !failed {
            screenshotPath := filepath.Join(browserScreenshotDir, "wrong_password_verify_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("验证错误密码失败场景失败", err, screenshotPath)
            return false
        }
        reportManager.EndStepSuccess("成功验证错误密码登录失败场景")

        // 测试场景3: 使用正确的凭据登录
        reportManager.StartStep("测试正确凭据登录")
        if err := loginPage.Login(loginConfig.Username, loginConfig.Password); err != nil {
            screenshotPath := filepath.Join(browserScreenshotDir, "login_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("登录失败", err, screenshotPath)
            return false
        }
        if success, err := loginPage.VerifyLoginSuccess(); err != nil || !success {
            screenshotPath := filepath.Join(browserScreenshotDir, "verification_failure.png")
            utils.TakeScreenshot(page, screenshotPath)
            reportManager.EndStepFailure("验证登录失败", err, screenshotPath)
            return false
        }
        reportManager.EndStepSuccess("成功验证正确凭据登录")

        return true
    }

    success := try()
    testDuration := time.Since(testStart)

    // 完成测试报告
    if success {
        reportManager.LogSuccess("登录测试成功", testDuration)
    } else {
        reportManager.LogFailure("登录测试失败", testDuration)
    }

    // 生成测试报告
    reportPath, err := reportManager.GenerateReport()
    if err != nil {
        log.Fatalf("生成测试报告失败: %v", err)
    }

    fmt.Printf("测试完成,报告已生成: %s\n", reportPath)
}

更多详细代码查看仓库:https://github.com/wan88888/playwright-go-demo

3.运行测试

本地运行

在终端执行命令:

go run main.go

测试报告

远程测试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2322669.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LeetCode hot 100 每日一题(13)——73. 矩阵置零

这是一道难度为中等的题目&#xff0c;让我们来看看题目描述&#xff1a; 给定一个 _m_ x _n_ 的矩阵&#xff0c;如果一个元素为 0 &#xff0c;则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 提示&#xff1a; m matrix.lengthn matrix[0].length1 < m, n …

Unity URP自定义Shader支持RenderLayer

前言&#xff1a; 当我们想用一个灯光只对特定的物体造成影响&#xff0c;而不对其余物体造成影响时&#xff0c;我们就需要设置相对应的LightLayer&#xff0c;但是这在URP12.0是存在的&#xff0c;在之后就不存在LightLayer这一功能&#xff0c;URP将其隐藏而改成了RenderLa…

Axure项目实战:智慧城市APP(完整交互汇总版)

亲爱的小伙伴&#xff0c;在您浏览之前&#xff0c;烦请关注一下&#xff0c;在此深表感谢&#xff01; 课程主题&#xff1a;智慧城市APP 主要内容&#xff1a;主功能&#xff08;社保查询、医疗信息、公交查询等&#xff09;、活动、消息、我的页面汇总 应用场景&#xff…

架构思维:预约抢茅子架构设计

文章目录 案例&#xff1a;预约抢茅子复杂度分析商品预约阶段等待抢购阶段商品抢购阶段订单支付阶段 技术方案商品预约阶段一、基于 Redis 单节点的分布式锁方案1. 核心流程2. 关键设计点 二、Redis 单节点方案的局限性1. 单点故障风险2. 主从切换问题 三、多节点 Redis 实现高…

基于SpringBoot+Vue的在教务管理(课程管理)系统+LW示例

1.项目介绍 系统角色&#xff1a;管理员、学生、教师功能模块&#xff1a;管理员&#xff08;学院管理、专业管理、班级管理、学生管理、教师管理、课程管理、选课修改&#xff09;、教师&#xff08;授课查询、教师课表、成绩录入&#xff09;、学生&#xff08;选修课程、学…

ubuntu桌面图标异常——主目录下的所有文件(如文档、下载等)全部显示在桌面

ubuntu桌面图标异常 问题现象问题根源系统级解决方案方法一:全局修改(推荐多用户环境)方法二:单用户修改(推荐个人环境)操作验证与调试避坑指南扩展知识参考文档问题现象 主目录文件异常显示 用户主目录(如/home/user/)下的所有文件(如文档、下载等)全部显示在桌面,…

sql结尾加刷题

找了一下mysql对extractvalue()、updatexml()函数的官方介绍https://dev.mysql.com/doc/refman/5.7/en/xml-functions.html#function_extractvalue ExtractValue(xml_frag, xpath_expr) 知识点 解释一下这两个参数xml_frag&#xff0c;是xml标记片段&#xff0c;第二个参数…

Linux学习笔记(应用篇三)

基于I.MX6ULL-MINI开发板 LED学习GPIO应用编程输入设备 开发板中所有的设备&#xff08;对象&#xff09;都会在/sys/devices 体现出来&#xff0c;是 sysfs 文件系统中最重要的目录结构 /sys下的子目录说明/sys/devices这是系统中所有设备存放的目录&#xff0c;也就是系统中…

【redis】事务详解,相关命令multi、exec、discard 与 watch 的原理

文章目录 什么是事务原子性一致性持久性隔离性 优势与 MySQL 对比用处 事务相关命令开启事务——MULTI执行事务——EXEC放弃当前事务——DISCARD监控某个 key——WATCH作用场景使用方法实现原理 事务总结 什么是事务 MySQL 事务&#xff1a; 原子性&#xff1a;把多个操作&am…

数据库基础知识点(系列七)

视图和索引相关的语句 1&#xff0e;引入视图的主要目的是什么? 答&#xff1a;数据库的基本表是按照数据库设计人员的观点设计的&#xff0c;并不一定符合用户的需求。SQL Server 2008可以根据用户需求重新定义表的数据结构&#xff0c;这种数据结构就是视图。视图是关系数据…

3.3 Taylor公式

1.定义 1.1 taylor公式 1.2 麦克劳林公式 1.3 推论 1.4 拉格朗日余项和皮亚诺型余项 2. 例题 3.几种特殊函数的麦克劳林展开

2000-2019年各省地方财政行政事业性收费收入数据

2000-2019年各省地方财政行政事业性收费收入数据 1、时间&#xff1a;2000-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、地方财政行政事业性收费收入 4、范围&#xff1a;31省 5、指标说明&#xff1a;地方财政行政事业…

Ftrans飞驰云联受邀参加“2025汽车零部件CIO年会“并荣获智象奖

2025年3月6日&#xff0c;由栖观汽车、栖观资讯和飞羽商务主办的“2025第二届中国汽车&零部件CIO年会暨智象奖颁奖盛典”于上海盛大召开&#xff0c;Ftrans飞驰云联作为国内领先的企业文件传输与数据交换解决方案提供商&#xff0c;受邀出席了年会&#xff0c;并凭借卓越的…

oracle查询归档日志使用量

1.统计最近30天的数据 SELECT TRUNC(first_time, DD) "日期", SUM(blocks * block_size) / 1024 / 1024 / 1024 "大小(GB)" FROM v$archived_log WHERE first_time > SYSDATE - 30 -- 统计最近30天的数据 GROUP BY TRUNC(first_time, DD) ORDER BY 1 D…

2025-03-26 学习记录--C/C++-PTA 6-3 求链式表的表长

合抱之木&#xff0c;生于毫末&#xff1b;九层之台&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、题目描述 ⭐️ 6-3 求链式表的表长 本题要求实现一个函数&#xff0c;求链式表的表长。 函数接口定义&#xff1a; &…

PHP框架 ThinkPHP 漏洞探测分析

目录 1. PHP历史利用最多的漏洞有哪些&#xff1f; 2. 如何在信息收集的过程中收到框架信息&#xff1f;有什么根据&#xff1f; 3. ThinkPHP框架漏洞扫描有哪些工具&#xff1f;红队攻击有哪些方式&#xff1f; 漏洞扫描工具 红队攻击方式 4. TPscan工具的主要作用及实际…

SylixOS 中 select 原理及使用分析

1、select接口简介 1.1 select接口使用用例 select 是操作系统多路 I/O 复用技术实现的方式之一。 select 函数允许程序监视多个文件描述符&#xff0c;等待所监视的一个或者多个文件描述符变为“准备好”的状态。所谓的”准备好“状态是指&#xff1a;文件描述符不再是阻塞状…

软考笔记——软件工程基础知识

第五章节——软件工程基础知识 软件工程基础知识 第五章节——软件工程基础知识一、软件工程概述1. 计算机软件2. 软件工程基本原理3. 软件生命周期4. 软件过程 二、软件过程模型1. 瀑布模型2. 增量模型3. 演化模型&#xff08;原型模型、螺旋模型)4. 喷泉模型5. 基于构建的开发…

FastGPT原理分析-数据集创建第二步:处理任务的执行

概述 文章《FastGPT原理分析-数据集创建第一步》已经分析了数据集创建的第一步&#xff1a;文件上传和预处理的实现逻辑。本文介绍文件上传后&#xff0c;数据处理任务的具体实现逻辑。 数据集创建总体实现步骤 从上文可知数据集创建总体上来说分为两大步骤&#xff1a; &a…