A TypeScript-first browser automation framework that lets you write readable, maintainable UI tests using a declarative DSL — then run them directly in the browser via a lightweight extension.
Tomation separates what you're testing from how elements are found on the page. You declare elements using a tag-based builder pattern, compose reusable tasks, and write tests that read like plain English. The compiler transforms your TypeScript source into a portable .tomation.json file that the browser extension executes step-by-step.
Want to see Tomation in action? Head over to the Tomation Playground — a set of small demo apps (login form, todo list, navigation) built specifically for testing the framework. Load the extension, open the playground in your browser, and run the included test specs to see step-by-step execution with visual feedback.
- Chrome Tomation Extension
- Firefox: Coming soon
For development, load the extension from packages/extension/dist as an unpacked extension.
npm install @tomationjs/compiler @tomationjs/dsl// tomation.config.ts
export default {
meta: {
name: 'My App Tests',
urls: ['http://localhost:3000'],
},
pom: './pom',
tests: './tests',
automations: './automations', // optional
baseUrl: './',
}// pom/login.pom.ts
import { is, idIs, Task, Type, TypePassword, Click } from '@tomationjs/dsl'
const usernameInput = is.INPUT.where(idIs('username')).as('Username')
const passwordInput = is.INPUT.where(idIs('password')).as('Password')
const submitButton = is.BUTTON.where(idIs('login-btn')).as('Submit')
const errorMessage = is.DIV.where(idIs('error-msg')).as('Error Message')
const message = is.DIV.where(idIs('message')).as('Message')
const fillCredentials = Task((params) => {
const { username, password } = params
Type(username).in(usernameInput)
TypePassword(password).in(passwordInput)
}).as('Fill Credentials')
const submit = Task(() => {
Click(submitButton)
}).as('Submit')
export default { usernameInput, passwordInput, submitButton, errorMessage, message, fillCredentials, submit }// tests/login.test.ts
import { Test, Click, AssertExists, AssertHasText } from '@tomationjs/dsl'
import Login from '~/pom/login.pom'
Test('Login with valid credentials', () => {
Login.fillCredentials({ username: 'admin', password: 'secret' })
Login.submit()
AssertHasText(Login.message, 'Login successful')
})
Test('Login shows error on empty submit', () => {
Click(Login.submitButton)
AssertHasText(Login.errorMessage, 'required')
})npx tomation compileThis produces a .tomation.json file (named from your meta.name) that the browser extension uses to execute your tests.
Open the Tomation browser extension panel, load your .tomation.json, and run tests interactively with step-by-step execution, pause/resume, and retry controls.
- TypeScript-first — Full editor autocomplete, type safety, and go-to-definition
- Declarative element selectors —
is.BUTTON.where(idIs('login')).as('Login Button') - XPath support —
Element('//div[@role="alert"]').as('Alert') - Reusable tasks — Compose multi-step workflows with parameters and conditionals
- Automations — Parameterized test procedures with a runtime form for user-provided values
- Folder-based namespacing — Organize POM files in folders without naming conflicts
- Browser extension runtime — Execute tests directly in the browser with visual feedback
- Watch mode —
npx tomation watchfor live recompilation during development - Lab: AI-Powered POM Generator — Point, click, generate. Select any element on the page and let AI write your Page Object Model files for you.
The Lab tab in the Tomation extension turns the tedious task of writing element selectors into a single click. Instead of manually inspecting the DOM, copying attributes, and hand-crafting POM files, you visually select a component and let your preferred AI service generate production-ready code in seconds.
- Inspect — Toggle inspect mode and hover over any element on the page. A visual overlay highlights what's under your cursor.
- Select — Click to capture the element and its full subtree.
- Generate — Hit "Generate POM" and Tomation sends the structural HTML (with sensitive data stripped) to your configured AI provider.
- Use — Copy or download the generated
.pom.tsfile and drop it into your test project.
- Zero boilerplate — Stop writing repetitive
is.TAG.where(...)patterns by hand. - Smart actions — The AI doesn't just map elements. It analyzes the component and generates reusable Task functions for common workflows (login, search, navigation, CRUD operations).
- Privacy-first — HTML is sanitized before it leaves your browser. User-entered values, emails, URLs with query params, and script contents are stripped or redacted.
- Bring your own AI — Works with OpenAI, Anthropic, Google Gemini, or any OpenAI-compatible endpoint. Your key, your choice.
- Cross-browser — Works on Chrome and Firefox using the same extension.
Page elements are declared using the is builder, which provides a fluent API for describing how to locate elements on the page. Elements are defined in POM (Page Object Model) files.
The pattern is is.TAG.where(matcher).as('Label'):
import { is, idIs, innerTextIs, classIncludes, placeholderIs, nameIs, typeIs } from '@tomationjs/dsl'
const submitButton = is.BUTTON.where(idIs('submit-btn')).as('Submit Button')
const emailInput = is.INPUT.where(nameIs('email')).as('Email Input')
const heading = is.H1.where(innerTextIs('Welcome')).as('Page Heading')Any uppercase HTML tag name works: is.INPUT, is.BUTTON, is.DIV, is.FORM, is.SELECT, is.SPAN, is.H1, etc.
The .where() method accepts a matcher factory that describes how to find the element:
| Matcher | Matches on | Example |
|---|---|---|
idIs(value) |
Element id attribute |
is.INPUT.where(idIs('username')) |
innerTextIs(value) |
Exact text content | is.BUTTON.where(innerTextIs('Login')) |
innerTextContains(value) |
Partial text content | is.DIV.where(innerTextContains('Welcome')) |
classIncludes(value) |
CSS class name | is.LI.where(classIncludes('active')) |
placeholderIs(value) |
Input placeholder | is.INPUT.where(placeholderIs('Enter email')) |
nameIs(value) |
Element name attribute |
is.INPUT.where(nameIs('password')) |
typeIs(value) |
Input type attribute |
is.INPUT.where(typeIs('checkbox')) |
valueIs(value) |
Element value property |
is.INPUT.where(valueIs('hello')) |
ariaLabel(value) |
aria-label attribute |
is.BUTTON.where(ariaLabel('Close')) |
roleIs(value) |
role attribute |
is.DIV.where(roleIs('dialog')) |
titleIs(value) |
title attribute |
is.A.where(titleIs('Submit form')) |
hrefContains(value) |
Substring of href attribute |
is.A.where(hrefContains('/login')) |
isDisabled() |
Element is disabled | is.BUTTON.where(isDisabled()) |
nthChild(n) |
Nth child position (1-based) | is.LI.where(nthChild(3)) |
dataAttr(name, value) |
data-* attribute |
is.DIV.where(dataAttr('testid', 'submit')) |
closestLabelIs(tag, text) |
Nearby label element | is.INPUT.where(closestLabelIs('LABEL', 'Email')) |
When multiple elements on the page match the same criteria, use .childOf(parent) to scope the search within a parent element:
const loginForm = is.FORM.where(idIs('login-form')).as('Login Form')
const submitButton = is.BUTTON.where(innerTextIs('Submit')).childOf(loginForm).as('Login Submit')
const signupForm = is.FORM.where(idIs('signup-form')).as('Signup Form')
const signupSubmit = is.BUTTON.where(innerTextIs('Submit')).childOf(signupForm).as('Signup Submit')The .childOf() and .where() methods can be chained in any order:
const child = is.INPUT.childOf(parentForm).where(typeIs('text')).as('Text Input')When a target element lacks unique identifiers, use .navigate(path) to reach it by traversing the DOM from a nearby identifiable anchor element. The method accepts a comma-separated string of navigation steps and is chainable with .where(), .childOf(), and .as() in any order.
Method signature: .navigate(path: string) — returns the builder for continued chaining.
Supported navigation steps:
| Step | Description |
|---|---|
parent |
Traverses to the parent element |
child[n] |
Traverses to the nth child element (1-based) |
firstChild |
Traverses to the first child element |
lastChild |
Traverses to the last child element |
nextSibling |
Traverses to the next sibling element |
prevSibling |
Traverses to the previous sibling element |
sibling[n] |
Traverses to the nth sibling (1-based, via parent's children) |
Examples:
import { is, idIs, textIs } from '@tomationjs/dsl'
// Simple path: find an anchor by ID, then navigate to a relative target
const target = is.DIV.where(idIs('anchor')).navigate('parent,child[2]').as('Target')
// Combined with childOf: scope the anchor within a parent, then navigate from it
const container = is.DIV.where(idIs('main-container')).as('Container')
const content = is.SPAN.childOf(container).where(textIs('Header')).navigate('nextSibling').as('Content')At runtime, the extension first resolves the anchor element using the standard tag + where logic, then applies each navigation step sequentially. If any step results in a null element, the extension reports an error indicating which step failed and its position in the path.
For complex selectors that can't be expressed with tag + where matchers, use XPath:
import { Element } from '@tomationjs/dsl'
const alert = Element('//div[@role="alert"]').as('Alert Box')
const thirdRow = Element('//table/tbody/tr[3]').as('Third Row')Or equivalently via the is proxy:
const alert = is.ELEMENT('//div[@role="alert"]').as('Alert Box')Simulate key presses on the focused element or a targeted element.
Shortcut functions (no arguments, press on focused element):
| Function | Key |
|---|---|
PressEnter() |
Enter |
PressTab() |
Tab |
PressEsc() |
Escape |
PressSpace() |
Space |
PressUp() |
ArrowUp |
PressDown() |
ArrowDown |
PressLeft() |
ArrowLeft |
PressRight() |
ArrowRight |
Generic functions:
| Function | Description |
|---|---|
PressKey(key, options?) |
Press any key on the focused element |
Press(key, options?).in(element) |
Press a key on a specific element |
The options object supports modifier keys: { ctrl?: boolean, alt?: boolean, meta?: boolean, shift?: boolean }
import { PressKey, Press, PressEnter, PressTab } from '@tomationjs/dsl'
// Shortcuts
PressEnter()
PressTab()
// Generic with modifiers
PressKey('a', { ctrl: true }) // Ctrl+A
PressKey('s', { meta: true }) // Cmd+S
// Targeted — press on a specific element
Press('Enter').in(searchInput)
Press('ArrowDown', { alt: true }).in(dropdown)Save actions extract dynamic values during test execution and store them in a per-run context store. Later steps can reference saved values using {{ctx.keyName}} template syntax.
import { SaveText } from '@tomationjs/dsl'
const confirmationCode = is.SPAN.where(idIs('confirmation-code')).as('Confirmation Code')
SaveText(confirmationCode).as('code')
// Later steps can use {{ctx.code}} to reference the saved textimport { SaveAttribute } from '@tomationjs/dsl'
const link = is.A.where(classIncludes('generated-link')).as('Generated Link')
SaveAttribute(link, 'href').as('linkUrl')
// {{ctx.linkUrl}} now contains the href valueimport { SaveValue } from '@tomationjs/dsl'
const orderIdInput = is.INPUT.where(idIs('order-id')).as('Order ID')
SaveValue(orderIdInput).as('orderId')
// {{ctx.orderId}} now contains the input's valueSave() lets you compute a value (using date helpers or template strings) and store it for later reference:
import { Save, today, tomorrow } from '@tomationjs/dsl'
Save(tomorrow()).as('appointmentDate')
Save(today('MM/DD/YYYY')).as('formattedToday')
Save('static-value').as('myConstant')Reference saved context values with {{ctx.keyName}} in any step that accepts a string:
Type('{{ctx.code}}').in(verificationInput)
AssertHasText(dateLabel, '{{ctx.appointmentDate}}')
Navigate('{{ctx.linkUrl}}')Context values persist for the entire test run across task boundaries, but reset between runs. Overwriting a key simply stores the new value — no error is produced.
Date helpers resolve to formatted date strings at test execution time, so your tests stay valid regardless of when they run.
Type(today()).in(dateInput) // today's date: 2025-07-06
Type(tomorrow()).in(dateInput) // +1 day
Type(yesterday()).in(dateInput) // -1 day
Type(nextWeek()).in(dateInput) // +7 days
Type(lastWeek()).in(dateInput) // -7 days
Type(nextMonth()).in(dateInput) // +30 days
Type(lastMonth()).in(dateInput) // -30 daysType(firstDateOfMonth(0)).in(dateInput) // 1st of current month
Type(lastDateOfMonth(0)).in(dateInput) // last day of current month
Type(firstDateOfMonth(-1)).in(dateInput) // 1st of previous month
Type(lastDateOfMonth(1)).in(dateInput) // last day of next monthAll date helpers accept an optional format string. The default is YYYY-MM-DD.
Type(today('MM/DD/YYYY')).in(dateInput) // 07/06/2025
Type(tomorrow('DD-MM-YYYY')).in(dateInput) // 07-07-2025
Type(firstDateOfMonth(0, 'M/D/YYYY')).in(dateInput) // 7/1/2025Supported tokens: YYYY (4-digit year), MM (zero-padded month), DD (zero-padded day), M (month), D (day). Separators (/, -, .) are preserved as-is.
Template literals with ${} expressions are evaluated at runtime, enabling dynamic value construction.
Type(`Hello ${username}`).in(greetingInput)Type(`Appointment on ${tomorrow()} at ${time}`).in(noteInput)Type(`Item ${count + 1}`).in(itemInput)
Type(`Total: ${price * quantity}`).in(totalInput)const bookAppointment = Task((params) => {
const { doctor, slot } = params
Type(`Dr. ${doctor} - ${tomorrow('MM/DD')} at ${slot}`).in(appointmentField)
}).as('Book Appointment')Automations are like Tests, but with typed parameters that the user fills in via a form in the browser extension before execution. Use them for reusable procedures where input values are determined at run-time rather than at authoring time.
Automations live in *.automation.ts files inside a configured automations directory:
// automations/todo.automation.ts
import { Automation, AssertExists, AssertHasText, SaveText } from '@tomationjs/dsl'
import Todo from '~/pom/todo.pom'
Automation('Add Todo Item', (params: { item: string }) => {
Todo.addItem({ text: params.item })
AssertExists(Todo.firstItem)
SaveText(Todo.firstItemText).as('savedItem')
AssertHasText(Todo.firstItemText, params.item)
})| Type annotation | Form input | Notes |
|---|---|---|
string |
Text input | Free-text field |
number |
Number input | Coerced to parseFloat before execution |
Date |
Date picker | Sent as ISO YYYY-MM-DD string |
'a' | 'b' | 'c' |
Select dropdown | Constrained to declared options |
Optional parameters use ? and won't block execution if left empty:
Automation('Create Account', (params: { email: string; environment?: string }) => {
// environment resolves to empty string if not provided
})String union literals render as a <select> dropdown in the extension panel:
Automation('Assign Role', (params: { username: string; role: 'admin' | 'user' | 'viewer' }) => {
// role is constrained to one of the declared options
})Add an automations path to your tomation.config.ts:
export default {
meta: { name: 'My App', urls: ['http://localhost:3000'] },
pom: './pom',
tests: './tests',
automations: './automations',
baseUrl: './',
}- You write an Automation with typed params in a
*.automation.tsfile - The compiler extracts parameter metadata (names, types, options) from TypeScript annotations
- The compiled
.tomation.jsonincludes anautomationsarray with param definitions and steps - The browser extension renders a form for the declared params when you select an Automation
- You fill in values, click Run, and the steps execute with your provided values resolved into
{{paramName}}placeholders - On successful completion, param values are remembered for next time
| Tests | Automations | |
|---|---|---|
| Values | Hardcoded at authoring time | Provided at run-time via form |
| File suffix | .test.ts |
.automation.ts |
| Declaration | Test('name', fn) |
Automation('name', fn) |
| Parameters | None | Typed params object |
| Use case | Regression checks | Reusable user-driven procedures |
packages/
compiler/ # CLI that compiles .ts POM/test files → .tomation.json
dsl/ # Runtime stubs + TypeScript types for authoring
extension/ # Browser extension (Chrome/Firefox) for test execution
examples/
playground/ # Static HTML apps for testing (deployed to GitHub Pages)
playground-tests/ # Tomation test scripts for the playground apps
my-app-tests/ # Example project with login flow tests
Tests and automations are displayed in the extension using the format:
sourceFile: label
Where:
- sourceFile is the relative path from the project root, with the top-level
tests/orautomations/directory stripped and file extensions removed (.test.ts,.automation.ts, etc.) - label is the name passed to
Test()orAutomation()
For example, a test defined in tests/login.test.ts with name 'Login with valid credentials' will display as:
login: Login with valid credentials
A test in a subfolder tests/auth/login.test.ts will display as:
auth/login: Login with valid credentials
And an automation in automations/todo.automation.ts with name 'Add Todo Item' will display as:
todo: Add Todo Item
This convention applies consistently across:
- The test list in the extension panel
- The test plan / step checklist view
- The execution log header during a run