基于TypeScript的浏览器自动化框架tsplay:从录制到工程化实践
1. 项目概述与核心价值
最近在折腾一些自动化脚本和工具链的集成,发现一个挺有意思的开源项目,叫tensafe/tsplay。乍一看这个名字,可能有点摸不着头脑,但如果你和我一样,经常需要处理一些需要“模拟”或“驱动”真实用户操作的任务,比如自动化测试、数据采集、或者RPA(机器人流程自动化),那这个工具很可能就是你一直在找的“瑞士军刀”。简单来说,tsplay是一个基于 TypeScript 的、高度可编程的浏览器自动化与操作录制/回放框架。它不像一些重量级的商业方案那样臃肿,也不像一些简单的脚本工具那样功能单一,而是在灵活性和功能性之间找到了一个很好的平衡点。
它的核心价值在于,让你能用熟悉的 TypeScript(或者 JavaScript)来编写复杂、稳定的自动化流程,并且自带了一套强大的录制器,可以“所见即所得”地生成操作脚本。这对于前端开发者、测试工程师,甚至是运营同学来说,都是一个降低自动化门槛的利器。想象一下,你不再需要为每一个网页操作去手动查找元素选择器,不再需要反复调试那些脆弱的 XPath,只需要像正常用户一样操作一遍浏览器,tsplay就能帮你把这一系列动作转换成可维护、可复用的代码。这不仅仅是效率的提升,更是思维模式的转变——从“如何用代码模拟点击”变成了“如何设计一个可靠的业务流程”。
2. 核心架构与设计哲学拆解
2.1 为什么是 TypeScript + Playwright?
tsplay的核心技术栈选择非常值得玩味。它没有重复造轮子去实现一个全新的浏览器引擎驱动,而是选择站在了巨人——Playwright的肩膀上。Playwright是微软开源的一个现代浏览器自动化库,支持 Chromium、Firefox 和 WebKit,提供了稳定、快速的 API。tsplay在此基础上,构建了一层更贴近业务操作和脚本管理的抽象。
选择 TypeScript 作为首要开发语言,是另一个关键决策。对于自动化脚本而言,可维护性和可靠性至关重要。TypeScript 的静态类型检查能在编写阶段就捕获大量潜在的错误,比如拼写错误的属性名、调用不存在的函数,或者参数类型不匹配。这对于动辄上百行的自动化脚本来说,无疑是巨大的福音。此外,良好的类型提示和代码自动补全,也能极大提升开发体验。tsplay充分利用了这一点,为几乎所有操作提供了完整的类型定义,让你在 IDE 里就能获得清晰的指引。
它的设计哲学可以概括为“录制生成骨架,编码注入灵魂”。录制功能负责解决“操作路径”的问题,快速生成基础脚本;而 TypeScript 编程能力则让你可以轻松地加入条件判断、循环、数据读取、错误处理等逻辑,让脚本变得智能和健壮。这种混合模式,既照顾了快速启动的需求,也满足了复杂场景下的定制化要求。
2.2 核心模块与工作流
tsplay的架构主要围绕几个核心模块展开:
录制器 (Recorder):这是一个独立的 Electron 应用或浏览器插件。启动后,它会注入到目标网页中,监听你的所有交互行为——点击、输入、滚动、跳转等,并将其实时转换为对应的
tsplayAPI 调用语句。录制过程中,你还可以手动添加断言(检查某个元素是否存在或内容是否正确)和等待条件,使得生成的脚本更具鲁棒性。核心运行时 (Core Runtime):这是一套 Node.js 库,提供了执行自动化脚本所需的所有 API。它封装并扩展了
Playwright的原生能力,例如提供了更智能的等待策略(等待元素可交互而非仅仅存在)、更便捷的页面对象模型(Page Object)支持,以及内置的截图、录屏和日志记录功能。脚本管理器与执行器 (Script Manager/Executor):负责组织、调度和执行你的 TypeScript 脚本。它支持将多个脚本组织成项目,管理依赖,并提供命令行工具来运行脚本。你可以配置并发执行、设置超时、重试策略,并生成详细的执行报告。
一个典型的工作流是这样的:你打开tsplay录制器,在目标网站上完成一遍你想要自动化的操作流程。录制器生成一个.ts文件。你打开这个文件,在生成的代码基础上,添加一些逻辑处理(比如从 CSV 文件读取数据作为输入,或者根据页面内容决定下一步操作)。最后,通过tsplay命令行工具运行这个脚本,即可在无头(Headless)或有头浏览器中自动复现整个流程。
注意:录制生成的代码虽然方便,但通常不是最优解。它可能包含一些冗余操作或不够健壮的选择器。有经验的开发者通常会以录制代码为起点,进行重构和优化,例如用更有语义的
>npm install -g @tensafe/tsplay-cli # 或使用 yarn # yarn global add @tensafe/tsplay-cli # 或使用 pnpm # pnpm add -g @tensafe/tsplay-cli安装完成后,我们可以创建一个新的自动化项目:
tsplay init my-automation-project cd my-automation-project这个命令会创建一个标准的项目目录结构,大致如下:
my-automation-project/ ├── package.json ├── tsconfig.json ├── playwright.config.ts # Playwright 配置文件 ├── tsplay.config.ts # tsplay 专属配置文件 ├── scripts/ # 存放你的自动化脚本 │ └── example.ts ├── fixtures/ # 存放测试数据或固定资源 ├── reports/ # 自动生成的执行报告和日志 └── node_modules/接下来,进入项目目录安装必要的依赖:
npm install # 此命令会安装 @tensafe/tsplay-core, playwright, 以及相关的类型定义
tsplay会自动安装适配的Playwright浏览器内核。如果是首次安装,这个过程可能会花费一些时间下载浏览器二进制文件。3.2 配置文件详解
两个配置文件值得关注:
playwright.config.ts和tsplay.config.ts。
playwright.config.ts继承自Playwright,你可以在这里配置所有浏览器相关的选项,例如使用哪种浏览器(chromium, firefox, webkit)、是否无头运行、视口大小、忽略 HTTPS 错误、设置代理等。tsplay对其进行了预设,通常无需大幅修改即可开始。
tsplay.config.ts则是tsplay自身的核心配置。这里可以定义一些全局行为:import { defineConfig } from '@tensafe/tsplay-core'; export default defineConfig({ // 脚本执行失败时的重试次数 retries: 2, // 每个操作(如 click, fill)的默认超时时间(毫秒) timeout: 30000, // 全局的等待策略:'load' | 'domcontentloaded' | 'networkidle' waitUntil: 'domcontentloaded', // 是否在每次操作前自动截图(用于调试) screenshotOnFailure: true, // 报告生成配置 reporter: [ ['html', { outputFolder: './reports/html' }], ['json', { outputFolder: './reports/json' }], ], // 可以在这里注入全局的 setup 和 teardown 脚本 globalSetup: './global-setup.ts', globalTeardown: './global-teardown.ts', });合理配置这些参数,对于稳定运行自动化脚本至关重要。例如,对于网络请求较多的单页应用(SPA),将
waitUntil设置为‘networkidle’可以确保页面完全加载完成后再执行下一步,避免因资源加载导致的元素找不到的错误。3.3 第一个脚本:录制与回放
让我们从一个最简单的例子开始:打开百度,搜索一个关键词。
首先,启动
tsplay的录制器:tsplay recorder这会打开一个独立的录制器窗口。在地址栏输入
https://www.baidu.com并回车。录制器会自动打开一个新浏览器标签页并开始录制。
- 在百度首页的搜索框里点击一下,输入 “tsplay 自动化”。
- 点击“百度一下”按钮。
- 等待搜索结果页面加载。
完成操作后,点击录制器上的“停止并生成代码”按钮。录制器会生成类似下面的 TypeScript 代码,并保存到
scripts目录下(例如search-baidu.ts):import { tsplay } from '@tensafe/tsplay-core'; tsplay.describe('百度搜索示例', () => { tsplay.it('应能成功搜索关键词', async ({ page }) => { // 1. 导航至百度 await page.goto('https://www.baidu.com'); // 录制器自动生成的元素选择器可能比较复杂 await page.click('input#kw'); await page.fill('input#kw', 'tsplay 自动化'); await page.click('input#su'); // 2. 等待页面导航或内容更新 await page.waitForURL('**/s**'); // 可以添加一个简单的断言,验证搜索结果标题包含关键词 await tsplay.expect(page.locator('h3')).toContainText(['tsplay', '自动化']); }); });现在,你可以关闭录制器,在命令行中运行这个脚本:
tsplay run scripts/search-baidu.ts你会看到
Playwright启动一个浏览器(默认是无头模式),自动执行上述所有操作,并在控制台输出执行结果。如果一切顺利,你会看到测试通过的提示。实操心得:第一次运行录制脚本很容易失败。常见原因一是网络延迟或页面加载慢,导致元素还未出现就尝试操作。建议在关键步骤后(如
goto或click导航后)主动添加page.waitForLoadState(‘networkidle’)或等待特定元素出现page.waitForSelector(‘.some-class’)。二是选择器不稳定,录制器生成的CSS选择器可能依赖于动态生成的ID或类名。最好的实践是,与前端开发约定使用稳定的>await page.click('button.submit-form'); // CSS 类选择器 await page.fill('//input[@name="username"]', 'admin'); // XPath文本选择器:通过元素文本内容定位,非常直观,但对空格和大小写敏感。 await page.click('text=登录'); // 点击文本为“登录”的元素 await page.click('text=/Log\s*in/i'); // 使用正则表达式匹配角色选择器 (Role Selector):根据 ARIA 角色定位,是语义化且稳定的好方法。 await page.click('role=button[name="提交"]'); await page.fill('role=textbox[name="用户名"]', 'user');数据属性选择器:最佳实践。与前端约定使用如 >// 前端元素:<input>// 等待某个加载中的 spinner 消失 await page.waitForSelector('.loading-spinner', { state: 'hidden' }); // 等待某个特定的 API 请求完成并获取其响应 const response = await page.waitForResponse(resp => resp.url().includes('/api/data')); const data = await response.json();自定义等待:对于更复杂的条件,可以使用 page.waitForFunction在浏览器上下文中执行判断。await page.waitForFunction(() => { const element = document.querySelector('.result-count'); return element && parseInt(element.textContent) > 0; });断言是验证自动化结果正确性的关键。
tsplay集成了丰富的断言库。import { tsplay } from '@tensafe/tsplay-core'; tsplay.describe('断言示例', () => { tsplay.it('各种断言方式', async ({ page }) => { await page.goto('https://example.com'); const title = page.locator('h1'); // 1. 使用 tsplay.expect (基于 Jest 风格) await tsplay.expect(title).toHaveText('Example Domain'); await tsplay.expect(page).toHaveURL('https://example.com'); // 2. 使用 Node.js 原生 assert const text = await title.textContent(); tsplay.assert.strictEqual(text, 'Example Domain'); // 3. 使用 Playwright 的 expect const { expect } = require('@playwright/test'); await expect(title).toHaveText('Example Domain'); }); });4.3 页面对象模型 (Page Object Model, POM)
对于中大型自动化项目,强烈推荐使用页面对象模型。这是一种设计模式,将每个页面的元素定位器和常用操作封装在一个类中,使测试脚本更清晰、更易维护。
tsplay对 POM 有很好的支持。我们可以为百度首页创建一个页面对象:// pages/BaiduHomePage.ts import { Page, Locator } from '@playwright/test'; export class BaiduHomePage { readonly page: Page; readonly searchBox: Locator; readonly searchButton: Locator; constructor(page: Page) { this.page = page; this.searchBox = page.locator('input#kw'); this.searchButton = page.locator('input#su'); } async navigate() { await this.page.goto('https://www.baidu.com'); } async search(keyword: string) { await this.searchBox.fill(keyword); await this.searchButton.click(); // 可以在这里封装等待结果页面加载的逻辑 await this.page.waitForURL('**/s**'); } async getSearchBoxPlaceholder(): Promise<string | null> { return await this.searchBox.getAttribute('placeholder'); } }然后在脚本中使用它:
// scripts/use-pom.ts import { tsplay } from '@tensafe/tsplay-core'; import { BaiduHomePage } from '../pages/BaiduHomePage'; tsplay.describe('使用POM进行搜索', () => { tsplay.it('应能通过POM完成搜索', async ({ page }) => { const baiduPage = new BaiduHomePage(page); await baiduPage.navigate(); // 现在脚本读起来就像业务描述,而不是一堆技术细节 await baiduPage.search('tsplay Page Object Model'); // 可以使用页面对象的方法进行断言 const placeholder = await baiduPage.getSearchBoxPlaceholder(); tsplay.expect(placeholder).toBe('百度一下'); }); });POM 模式极大地提升了代码的可读性和可维护性。当页面元素发生变化时,你只需要修改对应的页面对象类,而不需要到处修改脚本文件。
4.4 数据驱动与参数化
真正的自动化脚本很少是硬编码的。我们经常需要用不同的数据去执行相同的流程。
tsplay支持数据驱动测试。一种常见的方式是使用
tsplay.each或从外部文件(如 JSON, CSV)读取数据。// 使用 tsplay.each 进行参数化 import { tsplay } from '@tensafe/tsplay-core'; const searchKeywords = ['自动化测试', 'Playwright', 'TypeScript', 'RPA']; tsplay.describe('数据驱动搜索', () => { tsplay.each(searchKeywords, (keyword) => { tsplay.it(`使用关键词 "${keyword}" 进行搜索`, async ({ page }) => { const baiduPage = new BaiduHomePage(page); await baiduPage.navigate(); await baiduPage.search(keyword); // 对每个关键词的搜索结果进行一些通用断言 await tsplay.expect(page.locator('#content_left')).toBeVisible(); }); }); });对于更复杂的数据,可以从文件加载:
import * as fs from 'fs/promises'; import * as path from 'path'; import { parse } from 'csv-parse/sync'; interface TestData { username: string; password: string; expectedError?: string; } tsplay.describe('CSV数据驱动登录测试', () => { let testData: TestData[]; // 在 describe 块之前加载数据 tsplay.beforeAll(async () => { const csvFilePath = path.join(__dirname, '../fixtures/login-data.csv'); const fileContent = await fs.readFile(csvFilePath, 'utf-8'); testData = parse(fileContent, { columns: true, skip_empty_lines: true, }); }); tsplay.each(testData, (data) => { tsplay.it(`用户 ${data.username} 登录测试`, async ({ page }) => { // 使用 data.username, data.password 执行登录操作... // 根据 data.expectedError 进行断言... }); }); });5. 工程化实践与持续集成
5.1 项目结构与组织
一个维护良好的
tsplay项目应该有清晰的结构。以下是一个推荐的项目布局:e2e-automation/ ├── package.json ├── tsconfig.json ├── playwright.config.ts ├── tsplay.config.ts ├── scripts/ # 主脚本目录,按功能或模块划分 │ ├── smoke/ # 冒烟测试脚本 │ ├── regression/ # 回归测试脚本 │ └── api-flow/ # 涉及API调用的流程脚本 ├── pages/ # 页面对象模型 │ ├── BasePage.ts │ ├── HomePage.ts │ └── LoginPage.ts ├── components/ # 可复用的组件对象(如Header, Modal) │ └── Modal.ts ├── fixtures/ # 测试数据 │ ├── users.json │ └── products.csv ├── utils/ # 工具函数 │ ├──>import { defineConfig } from '@tensafe/tsplay-core'; export default defineConfig({ // ... use: { // 将所有配置传递给 page,可以在脚本中通过 page.context()._options 访问 // 或者定义一个全局的 baseURL baseURL: process.env.BASE_URL || 'https://dev.example.com', // 传递额外的自定义配置 extraHTTPHeaders: { 'Authorization': `Bearer ${process.env.API_TOKEN}`, }, }, // 可以定义多个项目,对应不同环境 projects: [ { name: 'development', use: { baseURL: 'https://dev.example.com' }, }, { name: 'staging', use: { baseURL: 'https://staging.example.com' }, }, ], });在脚本中,可以通过
page.context()._options或直接使用process.env来获取配置。更优雅的方式是创建一个配置管理模块:// utils/config.ts export interface Config { baseUrl: string; adminUser: { username: string; password: string }; apiTimeout: number; } export function getConfig(): Config { const env = process.env.NODE_ENV || 'development'; const configs: Record<string, Config> = { development: { baseUrl: 'http://localhost:3000', adminUser: { username: 'dev_admin', password: 'dev_pass' }, apiTimeout: 30000, }, staging: { baseUrl: 'https://staging.app.com', adminUser: { username: process.env.STAGING_USER!, password: process.env.STAGING_PASS! }, apiTimeout: 60000, }, production: { baseUrl: 'https://app.com', adminUser: { username: process.env.PROD_USER!, password: process.env.PROD_PASS! }, apiTimeout: 90000, }, }; return configs[env]; }5.3 集成到 CI/CD (GitHub Actions 示例)
自动化脚本只有集成到持续集成/持续部署流水线中,才能发挥最大价值。以下是一个简单的 GitHub Actions 工作流配置,用于在每次推送代码时运行自动化脚本:
# .github/workflows/run-e2e.yml name: E2E Automation Tests on: push: branches: [ main, develop ] pull_request: branches: [ main ] jobs: e2e-tests: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' cache: 'npm' - name: Install dependencies run: npm ci # 使用 ci 以获得更精确的依赖安装 - name: Install Playwright Browsers run: npx playwright install --with-deps chromium # 只安装需要的浏览器,加快速度 - name: Run tsplay automation tests run: npm run test:e2e # 假设你在 package.json 中配置了 "test:e2e": "tsplay run scripts/ --project=staging" env: BASE_URL: ${{ secrets.STAGING_BASE_URL }} STAGING_USER: ${{ secrets.STAGING_USER }} STAGING_PASS: ${{ secrets.STAGING_PASS }} NODE_ENV: staging - name: Upload test reports if: always() # 即使测试失败也上传报告 uses: actions/upload-artifact@v3 with: name: tsplay-html-report path: reports/html/ # tsplay 生成的 HTML 报告目录 retention-days: 7这个工作流完成了代码检出、环境搭建、依赖安装、浏览器安装、执行测试和上传报告的全过程。测试结果和详细的 HTML 报告会被保存为产物,方便事后查看失败原因。
6. 常见问题排查与性能优化
6.1 典型错误与解决方案
在实际使用中,你肯定会遇到脚本失败的情况。下面是一些常见错误及其排查思路:
错误现象 可能原因 排查步骤与解决方案 TimeoutError: page.click: Timeout 30000ms exceeded1. 元素选择器不正确或元素不存在。
2. 元素被遮挡、禁用或不可见。
3. 页面加载太慢,元素未出现。1. 使用浏览器开发者工具检查选择器是否能唯一定位到目标元素。优先使用 >Error: page.goto: net::ERR_CONNECTION_REFUSED目标网址无法访问。 1. 检查网络连接和 BASE_URL 配置是否正确。
2. 确认目标服务是否已启动。
3. 在playwright.config.ts中尝试设置ignoreHTTPSErrors: true(仅用于测试环境)。脚本在 CI 环境失败,本地却成功 1. CI 环境与本地环境差异(网络、资源、数据)。
2. CI 机器性能较差,超时时间不足。
3. 浏览器版本或驱动不一致。1. 在 CI 脚本中增加调试信息,如失败时截图、录屏、打印页面内容。
2. 适当增加全局或特定操作的timeout配置。
3. 确保 CI 环境中安装的浏览器版本与本地锁定的版本一致(playwright.config.ts中可配置)。
4. 使用tsplay的retry配置,对失败用例进行重试。元素状态不稳定,时而找到时而找不到 1. 页面存在动态内容或异步加载。
2. 使用了依赖于动态属性的选择器(如自动生成的 ID)。1.使用更稳定的选择器:这是根本。推动前端添加 >处理弹窗或新标签页失败脚本上下文没有切换到新窗口或弹窗。 1. 监听 page.on(‘popup’)事件来获取新窗口引用。
2. 使用Promise.all等待新窗口打开并获取其句柄:const [newPage] = await Promise.all([page.waitForEvent(‘popup’), page.click(‘a[target=”_blank”]’)])。6.2 性能优化与最佳实践
当脚本数量增多、流程变长时,性能和维护性就成为关键。
并行执行:
tsplay和Playwright支持并行运行测试。在tsplay.config.ts中设置workers: 4(根据机器 CPU 核心数调整),可以同时运行多个测试文件,大幅缩短总执行时间。注意,并行时要注意测试之间的隔离性,避免共享状态。复用浏览器上下文:启动和关闭浏览器开销很大。
tsplay默认会为每个测试文件复用同一个浏览器上下文,但每个it块会获得一个干净的页面(Page)。在globalSetup中登录,并将认证状态存储起来,然后在每个测试中加载该状态,可以避免重复登录。// global-setup.ts import { chromium, BrowserContext } from '@playwright/test'; import * as fs from 'fs'; async function globalSetup() { const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); // 执行登录操作 await page.goto('https://example.com/login'); await page.fill('#username', 'admin'); await page.fill('#password', 'password'); await page.click('button[type="submit"]'); // 等待登录成功,可以检查跳转或某个元素 await page.waitForURL('**/dashboard'); // 将认证状态保存到文件 await context.storageState({ path: 'storage-state.json' }); await browser.close(); } export default globalSetup;在
tsplay.config.ts中配置storageState: ‘storage-state.json’,这样每个新的页面都会携带登录状态。选择性截图与录屏:虽然截图和录屏对调试至关重要,但也会影响性能和产生大量文件。建议在配置中默认关闭,仅在失败时开启:
screenshot: ‘only-on-failure’, video: ‘retain-on-failure’。优化选择器:避免使用性能较差的 XPath 或过于复杂的 CSS 选择器。
Playwright的role选择器和text选择器通常性能不错且更稳定。使用page.locator(selector).first()或.last()可以精确定位。避免不必要的等待:用条件等待(
waitForSelector,waitForFunction)替代固定的page.waitForTimeout(5000)。后者会让脚本无条件等待指定时间,即使页面早已就绪,这会不必要地拉长执行时间。日志与监控:为关键步骤添加详细的日志,记录操作内容、耗时和结果。可以集成像
winston或pino这样的日志库,将日志输出到文件或监控系统,便于事后分析和性能瓶颈定位。7. 扩展与生态集成
tsplay的开放性允许它轻松集成到现有的工具链中。
与测试框架集成:虽然
tsplay自带了一套简单的describe/it风格语法,但你也可以将其与更强大的测试框架如Jest或Mocha结合使用,利用它们丰富的断言库、mock 能力和生命周期钩子。生成 Allure 报告:Allure 是一个非常流行的测试报告框架。可以通过社区插件
playwright-allure或自定义reporter,将tsplay的执行结果生成美观的 Allure 报告,包含步骤详情、截图、历史趋势等。与监控系统联动:可以将关键的自动化流程(如核心业务流冒烟测试)设置为定时任务,运行结果通过 Webhook 推送到监控平台(如 Grafana, Prometheus AlertManager)。一旦自动化失败,立即触发告警,实现主动监控。
自定义 Reporter:你可以编写自己的
reporter来定制输出格式。例如,将结果输出为团队内部约定的 JSON 格式,方便其他系统消费。// custom-reporter.ts import { Reporter } from '@tensafe/tsplay-core'; import { TestCase, TestResult } from '@playwright/test/reporter'; class MyCustomReporter implements Reporter { onTestEnd(test: TestCase, result: TestResult) { console.log(`Test ${test.title} finished with status: ${result.status}`); if (result.status === 'failed') { console.log(`Failure: ${result.error?.message}`); } } } export default MyCustomReporter;然后在配置中引用:
reporter: [‘./custom-reporter.ts’]。
tsplay作为一个聚焦于提升浏览器自动化开发体验的工具,其价值在于将录制、编码、执行和报告串联成一个流畅的工作流。它可能不是功能最全的,但它在易用性、开发效率和代码质量之间取得的平衡,对于许多团队和个人开发者来说,恰恰是那个“刚刚好”的选择。从快速录制一个简单流程,到构建一个覆盖核心业务、集成到 CI/CD 的自动化测试体系,tsplay都能提供有力的支撑。关键在于,理解其设计哲学,遵循最佳实践,并围绕它构建适合自己项目的工程化方案。
