playwright-go实战:自动化登录测试
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 configimport ("encoding/json""fmt""os""path/filepath"
)// BrowserConfig 浏览器配置
type BrowserConfig struct {Type string `json:"type"` // 浏览器类型:chromium, firefox, webkitHeadless 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"` // 登录URLInvalidUsername 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 pagesimport ("fmt""github.com/playwright-community/playwright-go"
)// LoginPage 表示登录页面对象
type LoginPage struct {page playwright.PageloginURL 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 mainimport ("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)}// 初始化Playwrightpw, 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.BrowserTypeswitch browserConfig.Type {case "firefox":browserType = pw.Firefoxcase "webkit":browserType = pw.WebKitdefault: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)// 设置登录URLloginPage.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
测试报告

远程测试

相关文章:
playwright-go实战:自动化登录测试
1.新建项目 打开Goland新建项目playwright-go-demo 项目初始化完成后打开终端输入命令: #安装项目依赖 go get -u github.com/playwright-community/playwright-go #安装浏览器 go run github.com/playwright-community/playwright-go/cmd/playwrightlatest insta…...
LeetCode hot 100 每日一题(13)——73. 矩阵置零
这是一道难度为中等的题目,让我们来看看题目描述: 给定一个 _m_ x _n_ 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 提示: m matrix.lengthn matrix[0].length1 < m, n …...
CEF 给交互函数, 添加控制台是否显示交互参数log开关
CEF 控制台添加一函数,枚举 注册的供前端使用的CPP交互函数有哪些 CEF 多进程模式时,注入函数,获得交互信息-CSDN博客 这两篇文章,介绍了注入函数,在控制台中显示 各自提供的交互函数信息。 有些场景下,我们还需要更详细的信息,比如想知道 彼此传递的参数, 如果每次调…...
云端存储新纪元:SAN架构驱动的智能网盘解决方案
一、企业存储的"不可能三角"破局 1.1 传统存储架构的困局 性能瓶颈:NAS架构在1000并发访问时延迟飙升300%容量限制:传统RAID扩容需停机维护,PB级存储扩展耗时超48小时成本矛盾:全闪存阵列每TB成本高达$3000࿰…...
PVE 安装黑苹果 MacOS
背景 我需要一台黑苹果,登录我不常用苹果账号。 方法 The Definitive Guide to Running MacOS in ProxmoxRunning a MacOS 15 Sequoia VM in ProxMox VE及视频 按照第二个的视频一步一步配置,第一个链接提供了不同版本OS...
Unity URP自定义Shader支持RenderLayer
前言: 当我们想用一个灯光只对特定的物体造成影响,而不对其余物体造成影响时,我们就需要设置相对应的LightLayer,但是这在URP12.0是存在的,在之后就不存在LightLayer这一功能,URP将其隐藏而改成了RenderLa…...
Axure项目实战:智慧城市APP(完整交互汇总版)
亲爱的小伙伴,在您浏览之前,烦请关注一下,在此深表感谢! 课程主题:智慧城市APP 主要内容:主功能(社保查询、医疗信息、公交查询等)、活动、消息、我的页面汇总 应用场景ÿ…...
LVS-DR模式配置脚本
LVS-DR模式配置脚本 实验环境,需要4台虚拟机 IP说明172.25.254.101客户端172.25.254.102负载均衡器DS172.25.254.103真实服务器RS172.25.254.104真实服务器RSVIP:172.25.254.255/32 系统必须有ipvsadm和ifconfig命令 dnf install ipvsadm dnf install n…...
树状数组 3 :区间修改,区间查询
【题目描述】 这是一道模板题。 给定数列 a[1],a[2],…,a[n],你需要依次进行q个操作,操作有两类: 1lrx:给定 l,r,x对于所有 i∈[l,r],将a[i]加上x(换言之,将 a[l],a[l1],…a[r] 分别加上 x&a…...
架构思维:预约抢茅子架构设计
文章目录 案例:预约抢茅子复杂度分析商品预约阶段等待抢购阶段商品抢购阶段订单支付阶段 技术方案商品预约阶段一、基于 Redis 单节点的分布式锁方案1. 核心流程2. 关键设计点 二、Redis 单节点方案的局限性1. 单点故障风险2. 主从切换问题 三、多节点 Redis 实现高…...
使用 gone.WrapFunctionProvider 快速接入第三方服务
项目地址:https://github.com/gone-io/gone 本文中源代码: esexamples/es 文章目录 1. gone.WrapFunctionProvider 简介2. 配置注入实现3. 实战示例:Elasticsearch 集成4. 使用方式5. 最佳实践6. 总结 在如何给Gone框架编写Goner组件…...
基于SpringBoot+Vue的在教务管理(课程管理)系统+LW示例
1.项目介绍 系统角色:管理员、学生、教师功能模块:管理员(学院管理、专业管理、班级管理、学生管理、教师管理、课程管理、选课修改)、教师(授课查询、教师课表、成绩录入)、学生(选修课程、学…...
gitee 常用指令
1.拉取代码 // http git clone http.........// https git clone https......... 2. 设置自己账户和密码 ----- 绑定git git config --global user.name "你的用户名"git config --global user.email "你的邮箱" 3. 上传本地代码至git git initgit r…...
etcd性能测试
etcd性能测试 本文参考官方文档完成etcd性能测试,提供etcd官方推荐的性能测试方案。 1. 理解性能:延迟与吞吐量 etcd 提供稳定、持续的高性能。有两个因素决定性能:延迟和吞吐量。延迟是完成一项操作所花费的时间。吞吐量是在某个时间段内…...
JIRA/Xray测试管理工具的最佳实践:从基础到高阶的全场景指南
引言:测试管理的数字化转型与工具价值 在数字化时代,软件质量已成为企业竞争力的核心指标。然而,传统的测试管理方式——如Excel记录用例、邮件沟通缺陷、手动执行回归测试——已无法满足快速迭代的敏捷开发需求。据统计,全球因测…...
ubuntu桌面图标异常——主目录下的所有文件(如文档、下载等)全部显示在桌面
ubuntu桌面图标异常 问题现象问题根源系统级解决方案方法一:全局修改(推荐多用户环境)方法二:单用户修改(推荐个人环境)操作验证与调试避坑指南扩展知识参考文档问题现象 主目录文件异常显示 用户主目录(如/home/user/)下的所有文件(如文档、下载等)全部显示在桌面,…...
AIP-191 文件和目录结构
编号191原文链接https://google.aip.dev/191状态批准创建日期2019-07-25更新日期2019-07-25 统一的文件和目录结构,虽然在技术上差别不大,但可以让用户和审查者更容易阅读API界面定义。 指南 注意 以下指南适合于使用protobuf定义的API,例如…...
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,是xml标记片段,第二个参数…...
Linux学习笔记(应用篇三)
基于I.MX6ULL-MINI开发板 LED学习GPIO应用编程输入设备 开发板中所有的设备(对象)都会在/sys/devices 体现出来,是 sysfs 文件系统中最重要的目录结构 /sys下的子目录说明/sys/devices这是系统中所有设备存放的目录,也就是系统中…...
LLM动态Shape实现原理与核心技术
LLM动态Shape实现原理与核心技术 目录 LLM动态Shape实现原理与核心技术1. **动态Shape核心原理**2. **实现方法与关键技术**3. **示例:vLLM处理动态长度输入**4. **动态Shape vs 静态Shape对比**5. **性能优化案例**总结`SamplingParams` 是什么常见参数及作用使用示例1. 动态…...
MyBatis 语法不支持 having 节点
MyBatis 不支持 having 节点 比如在 GROUP BY 之后添加了 HAVING 子句,其内容为SUM(vsbsad.business_income) > 0,该子句会对分组后的 SUM(vsbsad.business_income) 结果进行过滤,仅保留求和结果不为负数的分组记录。但是试过不支持。可把…...
【redis】事务详解,相关命令multi、exec、discard 与 watch 的原理
文章目录 什么是事务原子性一致性持久性隔离性 优势与 MySQL 对比用处 事务相关命令开启事务——MULTI执行事务——EXEC放弃当前事务——DISCARD监控某个 key——WATCH作用场景使用方法实现原理 事务总结 什么是事务 MySQL 事务: 原子性:把多个操作&am…...
数据库基础知识点(系列七)
视图和索引相关的语句 1.引入视图的主要目的是什么? 答:数据库的基本表是按照数据库设计人员的观点设计的,并不一定符合用户的需求。SQL Server 2008可以根据用户需求重新定义表的数据结构,这种数据结构就是视图。视图是关系数据…...
FreeRTOS 队列结构体 xQUEUE 深度解析
一、核心成员与功能设计 FreeRTOS 的队列结构体 xQUEUE 是任务间通信(IPC)的核心数据结构,通过统一的设计支持队列、信号量、互斥量等多种同步机制。其设计体现了 **"数据拷贝 结构复用"** 的理念,兼顾轻量化与扩展…...
3.3 Taylor公式
1.定义 1.1 taylor公式 1.2 麦克劳林公式 1.3 推论 1.4 拉格朗日余项和皮亚诺型余项 2. 例题 3.几种特殊函数的麦克劳林展开...
2000-2019年各省地方财政行政事业性收费收入数据
2000-2019年各省地方财政行政事业性收费收入数据 1、时间:2000-2019年 2、来源:国家统计局、统计年鉴 3、指标:行政区划代码、地区、年份、地方财政行政事业性收费收入 4、范围:31省 5、指标说明:地方财政行政事业…...
Ftrans飞驰云联受邀参加“2025汽车零部件CIO年会“并荣获智象奖
2025年3月6日,由栖观汽车、栖观资讯和飞羽商务主办的“2025第二届中国汽车&零部件CIO年会暨智象奖颁奖盛典”于上海盛大召开,Ftrans飞驰云联作为国内领先的企业文件传输与数据交换解决方案提供商,受邀出席了年会,并凭借卓越的…...
C++vector常用接口和模拟实现
C中的vector是一个可变容量的数组容器,它可以像数组一样使用[]进行数据的访问,但是又不像C语言数组空间是静态的,它的空间是动态可变的。 在日常中我们只需要了解常用的接口即可,不常用的接口查文档即可。 1.构造函数 //空构造…...
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…...
计算机二级WPS Office第七套WPS演示
解题过程...
