Getting Started
Installation
npm install craftdriver --save-devYour First Test
import { Browser } from 'craftdriver';
async function main() {
// Launch browser
const browser = await Browser.launch({ browserName: 'chrome' });
try {
// Navigate to a page
await browser.navigateTo('https://example.com');
// Get page title
const title = await browser.title();
console.log('Page title:', title);
// Click a link
await browser.click('a');
// Fill a form
await browser.fill('#email', 'test@example.com');
await browser.click('#submit');
// Assert result
await browser.expect('#message').toHaveText('Success!');
} finally {
await browser.quit();
}
}
main();browser.X() vs page.X() — when to use which
browser.click(), browser.find(), browser.fill(), browser.evaluate() and the other DOM-touching shortcuts on Browser are sugar for (await browser.activePage()).X(...). They always target the focused page in browser.defaultContext. They never cross into a Page that lives inside a context returned by browser.newContext().
For single-tab tests this is exactly what you want — the shortcuts keep your code short:
await browser.navigateTo('https://example.com');
await browser.click('#submit');For multi-tab or multi-user tests, get a Page explicitly:
// Multi-tab
const page = await browser.openPage({ url: '/help', type: 'tab' });
await page.click('#close'); // unambiguous
// Multi-user (isolated profiles)
const alice = await browser.newContext();
const aPage = await alice.newPage({ url: '/login' });
await aPage.fill('#user', 'alice'); // browser.fill() would target the
// default context, NOT alice.If you need to be explicit even in single-tab code, browser.activePage() returns the same Page the shortcuts would target.
Launch Options
The simplest launch runs Chrome:
const browser = await Browser.launch(); // Chrome by defaultPick a browser with browserName:
const browser = await Browser.launch({
browserName: 'firefox', // 'chrome' | 'chromium' | 'firefox' | 'safari'
// BiDi: restore cookies + multi-origin localStorage before first navigation.
storageState: '.auth/session.json', // path or in-memory SessionState
});WebDriver Classic cannot restore non-empty state at launch. For its strict single-origin fallback, navigate first and call browser.loadState(); see Session Management.
To switch browsers from an environment variable in your own tests, read it yourself and pass it to browserName — craftdriver doesn't read any browser env var for you:
const browser = await Browser.launch({
browserName: process.env.BROWSER_NAME ?? 'chrome',
});Safari
Safari runs on macOS with the same API, as a smaller Classic-only tier. Enable automation once (safaridriver --enable) and launch:
const browser = await Browser.launch({ browserName: 'safari' });
await browser.navigateTo('http://127.0.0.1:8080/login.html');
await browser.fill('#username', 'alice');
await browser.click('#submit');
await browser.quit();Safari is macOS-only, headed, and serial (one session at a time), and doesn't support BiDi-only features (network mocking, tracing, emulation, …). See the Safari guide for the full picture — setup, what works, and Safari Technology Preview.
Custom WebDriver binary or port
Browser.launch() resolves the driver binary automatically. To pin a specific binary, change the host/port, or pass extra command-line arguments to the driver, construct a ChromeService or FirefoxService and pass it via chromeService / firefoxService:
import { Browser, ChromeService, FirefoxService } from 'craftdriver';
// Pin a specific chromedriver binary on a fixed port
const browser = await Browser.launch({
browserName: 'chrome',
chromeService: new ChromeService({
binaryPath: '/opt/chromedriver/chromedriver',
port: 9515,
hostname: '127.0.0.1',
args: ['--log-level=ALL'],
}),
});
// Custom geckodriver path for Firefox
const ff = await Browser.launch({
browserName: 'firefox',
firefoxService: new FirefoxService({
binaryPath: '/usr/local/bin/geckodriver',
}),
});Without an explicit binaryPath, both services check (in order) the matching env var (CHROMEDRIVER_PATH / GECKODRIVER_PATH), node_modules/.bin/, then $PATH. If port is omitted, a free port is picked automatically.
With Vitest
import { describe, it, beforeEach, afterEach } from 'vitest';
import { Browser } from 'craftdriver';
describe('My App', () => {
let browser: Browser;
beforeEach(async () => {
browser = await Browser.launch({ browserName: 'chrome' });
});
afterEach(async () => {
await browser.quit();
});
it('shows welcome message after login', async () => {
await browser.navigateTo('http://localhost:3000/login');
await browser.fill('#username', 'testuser');
await browser.fill('#password', 'password123');
await browser.click('#login-btn');
await browser.expect('#welcome').toContainText('Hello, testuser');
});
});Next Steps
- Browser API - Full browser control reference
- Selectors - Finding elements with CSS, XPath, and semantic locators
- Session Management - Save/restore login sessions