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

1.新建项目

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

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

bash 复制代码
#安装项目依赖
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

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

XML 复制代码
{
  "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

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

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.运行测试

本地运行

在终端执行命令:

bash 复制代码
go run main.go

测试报告

远程测试
相关推荐
研究司马懿10 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
依米阳光082 天前
Playwright MCP AI实现自动化UI测试
ui·自动化·playwright·mcp
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
feasibility.6 天前
playwright爬虫采集京东商品主页数据(含xpath定位示例)
爬虫·playwright