BugmoleDocs bugmole.com →

Sample tests

Bugmole records every journey it discovers as a readable flow file. The flow is the source of truth: it is what Bugmole runs across browsers and devices, and what you review and edit. This page shows one journey — creating a project — as a flow, then the same journey written by hand in Playwright, Cypress, and Selenium for comparison.

Bugmole writes flows, not Playwright, Cypress, or Selenium code, and it does not export flows to those frameworks. Bugmole's own runner plays each flow in a real browser, using Playwright under the hood.

The flow

Flows you run from the CLI live in your repository. bugmole --mode suite looks in spec/flows/ first, then flows/, then .maestro/. Flows created in the dashboard, such as the ones exploration writes, are kept in the project's Bugmole storage. Each flow has a header that says where it starts, followed by the steps a real user takes.

url: https://staging.example.com/projects
name: create-project
---
- launchApp
- tapOn: "New project"
- tapOn: "Project name"
- inputText: "My Project"
- tapOn: "Create"
- assertVisible: "Project created"

Steps find elements the way a person does: by a button's name, a field's label or placeholder, or visible text. There are no CSS selectors to keep up to date, so a restyled page does not break the test.

Step What it does
launchApp Opens the flow's URL
openLink: "/settings" Navigates to a path or URL
tapOn: "Save" Clicks the button, link, or field with that name
tapOn: { text: "Delete", within: "API_TOKEN" } Clicks the "Delete" in the row or card that also shows "API_TOKEN", not the first "Delete" on the page
inputText: "hello" Types into the focused field
eraseText Clears the focused field
selectOption: { field: "Plan", option: "Business" } Picks an option in the drop-down with that label, by the option's text (or value). Add index when several drop-downs share a label
pressKey: "Enter" Presses a key
assertVisible: "Saved" Waits for text to appear
assertNotVisible: "Error" Checks text is absent
assertClickable: "Continue" Checks a control can be used
scrollUntilVisible: { element: "Footer" } Scrolls until the element shows
extendedWaitUntil: { visible: "Ready", timeout: 20000 } Waits longer for slow screens
assertLayout Checks the page looks right at this screen size: it doesn't scroll sideways, and no text spills out of its box or is cut off without an ellipsis
assertLinks Checks every link to your own site on the page answers. assertLinks: { external: true } checks links to other sites too: a 404, 410 or server error fails, and sites that refuse automated visitors aren't counted
takeScreenshot: "after-save" Saves a named screenshot
requestTempEmail / openEmailLink Tests sign-up and email links with a real disposable inbox (see below)

Confirm dialogs

When a step opens a browser dialog (confirm, alert or prompt), the flow answers it the way a person clicking OK would: a confirm-guarded Delete really deletes, and a prompt gets its default value. Each answer is written to the run's log, for example Dialog "Sure?" accepted. To test the Cancel path instead, add dialogs: dismiss to the flow's header:

url: https://staging.example.com/projects
dialogs: dismiss
---
- launchApp
- tapOn: "Delete project"
- assertVisible: "My Project"

requestTempEmail creates a real inbox and lets later steps type its address as {{tempEmail}}. openEmailLink waits for a message at that inbox and opens the first link in it:

- requestTempEmail
- tapOn: "Email"
- inputText: "{{tempEmail}}"
- tapOn: "Sign up"
- openEmailLink: { subject: "Confirm", timeout: 60000 }
- assertVisible: "Email confirmed"

Passwords and secrets

Type secrets by reference, never literally:

- tapOn: "Password"
- inputText: "{{secret.APP_PASSWORD}}"

Set BUGMOLE_FLOW_SECRET_APP_PASSWORD on the machine or CI job that runs the flow. A flow can only read variables with the BUGMOLE_FLOW_SECRET_ prefix, and only inputText can use them. Everything typed is masked in logs and results.

When a run targets an environment, the flow's URL is moved onto that environment, so the same flow runs against local, preview, staging, or production. See Configuration.

Run it

bugmole --mode suite --flows spec/flows --base-url https://staging.example.com \
  --browsers chromium,firefox,webkit,msedge --devices "iPhone 15,Pixel 7"

Every browser and device runs in parallel, and each one keeps its own screenshots, video, and log.

For comparison: the same journey written by hand

This is what the flow above would look like if you wrote it yourself in each framework. Bugmole does not generate or run these files; they are here so you can see how a flow maps to code you may already know.

Playwright

import { test, expect } from '@playwright/test';

test('user can create a new project', async ({ page }) => {
  await page.goto('/projects');
  await page.getByRole('button', { name: 'New project' }).click();
  await page.getByLabel('Project name').fill('My Project');
  await page.getByRole('button', { name: 'Create' }).click();
  await expect(page.getByText('Project created')).toBeVisible();
});

Cypress

describe('projects', () => {
  it('user can create a new project', () => {
    cy.visit('/projects');
    cy.contains('button', 'New project').click();
    cy.get('input[name="projectName"]').type('My Project');
    cy.contains('button', 'Create').click();
    cy.contains('Project created').should('be.visible');
  });
});

Selenium (JavaScript)

import { Builder, By, until } from 'selenium-webdriver';
import assert from 'node:assert/strict';

const baseUrl = process.env.BASE_URL ?? 'https://staging.example.com';

describe('projects', function () {
  let driver;
  before(async () => { driver = await new Builder().forBrowser('chrome').build(); });
  after(async () => { await driver.quit(); });

  it('user can create a new project', async () => {
    await driver.get(`${baseUrl}/projects`);
    await driver.findElement(By.xpath("//button[normalize-space()='New project']")).click();
    await driver.findElement(By.name('projectName')).sendKeys('My Project');
    await driver.findElement(By.xpath("//button[normalize-space()='Create']")).click();
    const toast = await driver.wait(until.elementLocated(By.xpath("//*[normalize-space()='Project created']")), 10000);
    assert.ok(await toast.isDisplayed());
  });
});