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