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, and the same journey written in Playwright, Cypress, and Selenium.

The flow

Flows live in your repository (by default under spec/flows/). Each one 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
inputText: "hello" Types into the focused field
eraseText Clears the focused field
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
takeScreenshot: "after-save" Saves a named screenshot
requestTempEmail / openEmailLink Tests sign-up and email links with a disposable inbox

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.

The same journey in your framework

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());
  });
});