GuidesPlaywright Testing

Playwright Testing

Set any trail to both mode and it runs as a Playwright regression test in CI from the same JSON file. The tour that guides new users through your product is also the test that verifies your product still works on every deploy.

No separate test suite. No duplicate work.

@trailguide/playwright requires @trailguide/core >= 0.1.8 and @playwright/test >= 1.40.0.


Setup

Install the runner

npm install --save-dev @trailguide/playwright

Set trail mode to both

Add mode: "both" to any trail you want to run as a test. Optionally add action and assert fields to steps:

{
  "id": "welcome",
  "title": "Welcome Tour",
  "version": "1.0.0",
  "mode": "both",
  "steps": [
    {
      "id": "step-1",
      "target": "[data-trail-id='create-btn']",
      "placement": "bottom",
      "title": "Create a project",
      "content": "Click here to get started.",
      "action": "click",
      "assert": { "type": "visible" }
    },
    {
      "id": "step-2",
      "target": "[data-trail-id='project-name']",
      "placement": "right",
      "title": "Name it",
      "content": "Give your project a name.",
      "action": "fill",
      "value": "My project",
      "assert": { "type": "value", "expected": "My project" }
    }
  ]
}

Write a Playwright test

import { test } from '@playwright/test'
import { runTrail } from '@trailguide/playwright'
import welcomeTrail from './tours/welcome.json'
 
test('welcome tour', async ({ page }) => {
  await page.goto(process.env.BASE_URL ?? 'http://localhost:3000')
  await runTrail(page, welcomeTrail)
})

Add to CI

# .github/workflows/test.yml
- name: Run trail tests
  run: npx playwright test

How runTrail works

runTrail(page, trail, options?) walks each step in order. For each step:

  1. Navigates to step.url if set and the page is not already there
  2. Waits for the target element to be visible, then executes step.action
  3. Runs step.wait condition if defined
  4. Runs step.assert if defined
  5. Falls back to checking the element is attached to the DOM if neither action nor assert is set

If any step fails (missing selector, failed action, or failed assertion) the test fails and the deploy is blocked.

import { runTrail, type RunTrailOptions } from '@trailguide/playwright'
 
await runTrail(page, trail, {
  baseUrl: 'https://staging.myapp.com',  // prepended to step.url if relative
  timeout: 8000,                          // per-step timeout in ms (default: 8000)
  test,                                   // Playwright test object — wraps each step in test.step()
  notify: {                               // send alerts on failure
    slack: process.env.SLACK_WEBHOOK_URL,
    webhook: process.env.ALERT_WEBHOOK_URL,
    trailUrl: 'https://app.gettrailguide.com',
  },
  reportUrl: process.env.TRAILGUIDE_REPORT_URL, // post run results to the test health dashboard
  apiKey: process.env.TRAILGUIDE_API_KEY,
})

Actions

The action field tells Playwright what to do on the step’s target element.

actionPlaywright methodNotes
'click'locator.click()Default interaction
'rightClick'locator.click({ button: 'right' })Opens context menus
'dblclick'locator.dblclick()Double-click
'hover'locator.hover()Mouse over, no click
'focus'locator.focus()Keyboard focus without clicking
'fill'locator.fill(value)Clears and types into an input
'type'locator.pressSequentially(value)Types character by character
'press'locator.press(value)Keyboard shortcut, e.g. "Enter", "Control+s"
'select'locator.selectOption(value)Selects a <select> option
'check'locator.check()Checks a checkbox or radio
'uncheck'locator.uncheck()Unchecks a checkbox
'scroll'el.scrollBy(dx, dy)value is "deltaX,deltaY", e.g. "0,300"
'dragTo'locator.dragTo(target)value is the CSS selector of the drop target
'setInputFiles'locator.setInputFiles(value)value is the file path
'goto'page.goto(value)Navigate to value URL; no locator needed
'evaluate'page.evaluate(value)Execute arbitrary JavaScript string

Multi-tab support

When an action opens a new browser tab, set opensNewTab: true on that step. runTrail waits for the new page to open and switches context to it automatically.

{
  "id": "open-tab",
  "target": "[data-trail-id='external-link']",
  "action": "click",
  "opensNewTab": true
}

To switch back to a previously opened tab, use tabContext: "main" (original page) or tabContext: "new" (most recently opened tab).

{
  "id": "back-to-main",
  "target": "[data-trail-id='header']",
  "tabContext": "main",
  "assert": { "type": "visible" }
}

Assertions

The assert field runs a Playwright assertion after the action completes.

typeAssertionRequires expected
'visible'Element is visibleNo
'hidden'Element is hiddenNo
'enabled'Element is enabled (not disabled)No
'disabled'Element is disabledNo
'checked'Checkbox or radio is checkedNo
'empty'Input or element has no contentNo
'text'Element text matches exactlyYes
'containsText'Element text contains substringYes
'value'Input value matchesYes
'hasClass'Element has a CSS classYes
'attribute'Element attribute matchesattribute + expected
'count'Number of matching elementsYes (number as string)
'url'Page URL matchesYes
'title'Page title matchesYes
'screenshot'Visual screenshot comparisonNo
'custom'Run a custom JS assertionYes (JS expression)

For 'attribute', set attribute to the attribute name and expected to the expected value:

{
  "assert": {
    "type": "attribute",
    "attribute": "aria-expanded",
    "expected": "true"
  }
}

Wait conditions

Use wait on a step to pause before running the assertion. This is useful after navigations, animations, or async data loads.

typeWaits forvalue
'networkIdle'No network requests for 500msNot used
'load'load eventNot used
'domcontentloaded'DOMContentLoaded eventNot used
'selector'Element to appear in DOMCSS selector
'timeout'Fixed delayMilliseconds as string
{
  "id": "after-submit",
  "target": "[data-trail-id='success-banner']",
  "action": "click",
  "wait": { "type": "networkIdle" },
  "assert": { "type": "visible" }
}
{
  "id": "wait-for-modal",
  "target": "[data-trail-id='open-modal']",
  "action": "click",
  "wait": { "type": "selector", "value": ".modal" },
  "assert": { "type": "visible" }
}

Steps without action or assert

Steps that have no action or assert still run in CI. runTrail checks that the target element is attached to the DOM. This catches the most common regression: a selector that no longer matches anything after a code change.

{
  "id": "step-nav",
  "target": "[data-trail-id='main-nav']",
  "placement": "bottom",
  "title": "Navigation",
  "content": "Find everything you need here."
}

No action. No assert. The test still fails if [data-trail-id='main-nav'] disappears.


Named steps in the reporter

Pass the Playwright test object to wrap each step in test.step(). Every step title appears as a named sub-step in the Playwright HTML reporter and CI output — no extra configuration needed.

import { test } from '@playwright/test'
import { runTrail } from '@trailguide/playwright'
import welcomeTrail from './tours/welcome.trail.json'
 
test('welcome tour', async ({ page }) => {
  await runTrail(page, welcomeTrail, { test })
})

When a step fails, the reporter highlights exactly which named step broke and shows the error inline.


Failure notifications

Get alerted immediately when a trail test fails in CI. Configure notify with a Slack webhook, a generic HTTP endpoint, or both:

await runTrail(page, trail, {
  notify: {
    slack: process.env.SLACK_WEBHOOK_URL,
    webhook: process.env.ALERT_WEBHOOK_URL,
    trailUrl: 'https://app.gettrailguide.com',
  },
})

On any step failure, Trailguide POSTs a payload before re-throwing the error so Playwright still fails the test:

{
  "trailId": "onboarding",
  "trailTitle": "Welcome Tour",
  "stepIndex": 2,
  "stepTitle": "Click the create button",
  "error": "Timeout waiting for selector '[data-trail-id=\"create-btn\"]'",
  "trailUrl": "https://app.gettrailguide.com"
}

Slack

Pass any Slack incoming webhook URL. The message includes the trail name, failing step, and a link back to the trail if trailUrl is set.

Generic webhook

webhook receives the same JSON payload via POST. Works with PagerDuty, Datadog, custom Slack apps, or any HTTP endpoint.

GitHub Actions setup

- run: npx playwright test
  env:
    BASE_URL: ${{ secrets.BASE_URL }}
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Test health dashboard (Pro)

Track pass rates, failure trends, and most-broken steps over time across your entire trail suite.

Generate an API key

Open Dashboard > Tests in the Pro Editor, click Generate API Key, and copy the key.

Add to CI secrets

GitHub Actions: Settings > Secrets and variables > Actions > New repository secret

  • Name: TRAILGUIDE_API_KEY
  • Value: your tg_... key

GitLab CI: Settings > CI/CD > Variables > Add variable

  • Key: TRAILGUIDE_API_KEY
  • Value: your tg_... key

Wire up runTrail

await runTrail(page, trail, {
  reportUrl: 'https://app.gettrailguide.com/api/test-runs',
  apiKey: process.env.TRAILGUIDE_API_KEY,
})

After each run, the dashboard updates with pass/fail status, duration, which step failed, and the base URL that was tested. The Most broken steps panel shows which steps fail most often across all trails so you know where to focus fixes.


Using the Pro Editor

The Pro Editor has a mode toggle on every trail (Guide / Test / Both) and exposes action, value, assert, wait, and tab context fields per step when the mode is set to test or both. You can build and update test trails visually without touching JSON.

When recording in test or both mode, the extension automatically captures a wide range of interactions:

  • Form fields (input, textarea): click to focus, type your value, then Tab or click away. Captured as action: "fill" with the typed value.
  • Dropdowns (select): choose an option. Captured as action: "select" with the selected value.
  • Right-click menus: right-click an element to capture it as action: "rightClick", then click the menu item as a separate click step.
  • Double-click: double-click an element to capture it as action: "dblclick".
  • Hover: Alt + click to capture as action: "hover" without triggering a navigation.
  • Keyboard shortcuts: pressing Enter, Tab, arrow keys, F-keys, or Ctrl/Cmd combos is captured as action: "press" with the key combination.
  • Scroll: scrolling the page or a scrollable container is captured as action: "scroll" with the pixel offset.

Every trail in the Pro Editor is stored with a screenshot of each step. When your UI ships a redesign, anyone on your team can open the trail, see exactly what changed, fix the affected steps, and push the update to GitHub or GitLab as a pull request without an engineering ticket.