nordweb
v1.0.3
Published
A low-overhead TypeScript web UI automation API powered by Chrome DevTools Protocol.
Maintainers
Readme
NordWeb
A low-overhead TypeScript web UI automation API built directly on Chrome DevTools Protocol, without Playwright.
Setup
npm install
npm run build
npm testRun NordWeb test files directly with the CLI:
npm run build
nordweb test
nordweb test --grep login --workers 2 --retries 1 --timeout 15000 --reporter dotAfter installing NordWeb from npm, the same commands are available through npx:
npx nordweb init
npx nordweb test
npx nordweb test --reporter htmlnpx nordweb init creates a starter nordweb.config.ts, an @smoke example test, a test-results directory, and a .gitignore. It does not overwrite existing files. Initialize another directory with npx nordweb init ./my-tests or use npx nordweb init --dir ./my-tests --force to replace generated files.
The CLI discovers *.test.ts and *.spec.ts files under tests/ by default. A test module can export a NordTestCase, an array named tests, or a default array:
import type { NordTestCase } from "nordweb";
export const beforeEach = async (page) => page.goto("https://example.com");
export const afterEach = async (page) => page.screenshot("test-results/last.png");
export const tests: NordTestCase[] = [
{ name: "home page", tags: ["@smoke"], run: async (page) => {
await page.getByRole("heading", { name: "Example Domain" }).waitFor();
} }
];Create nordweb.config.ts to set defaults:
export default {
testDir: "tests",
workers: 2,
retries: 1,
timeout: 15_000,
reporter: "line",
artifactDir: "test-results",
networkConditions: "slow3g"
};Supported reporters are line, dot, json, junit, and html. The HTML reporter writes a summary page to test-results/report.html and an artifact index to test-results/trace-index.html (or your configured artifactDir). Use --artifact-dir to choose the output directory; CLI flags override config values.
Install or select a browser for the current user:
npx nordweb install
npx nordweb install --browser chromium
npx nordweb install --path "C:\\path\\to\\chrome.exe"The install command detects an existing Chrome or Chromium executable and stores its path in the current user's NordWeb directory. It does not perform a machine-wide browser installation. Use --path when the browser is installed in a custom location.
Throttle browser network traffic with the built-in profiles:
npx nordweb test --network slow3g
npx nordweb test --network offlineFor custom bandwidth, pass networkConditions in a context or config:
const context = await browser.newContext({
networkConditions: {
latency: 150,
downloadKbps: 1_500,
uploadKbps: 750,
connectionType: "cellular4g"
}
});latency is measured in milliseconds, while upload and download speeds are measured in kilobits per second.
Tag tests with @smoke, @regression, or @slow, then select matching tests from the CLI. The --tag option can be repeated:
npx nordweb test --tag smoke
npx nordweb test --tag @regression
npx nordweb test --tag smoke --tag regressionTests can define tags with or without the @ prefix. Tag matching is case-insensitive.
Group tests in reports with metadata:
export const tests: NordTestCase[] = [{
name: "login works",
tags: ["@smoke"],
feature: "Authentication",
owner: "web-team",
run: async (page) => { /* ... */ }
}];HTML reports automatically group results by tag, file, feature, owner, and browser. Focus on one dimension with:
npx nordweb test --reporter html --group-by featureSupported grouping dimensions are tag, file, feature, owner, and browser.
NordWeb can warn when a test becomes significantly slower than its previous duration. The CLI stores durations in test-results/duration-baseline.json and compares future runs against that baseline:
npx nordweb test --reporter html --duration-warning-ms 250 --duration-warning-percent 0.5Warnings are included in NordTestResult.durationWarning and highlighted in HTML and JSON output.
AI locator repair
Enable locator repair when selectors may change between deployments. NordWeb sends a compact list of visible DOM candidates to your provider and only accepts a repair that meets the configured confidence threshold and matches exactly one element:
const context = await browser.newContext({
locatorRepair: {
enabled: true,
minConfidence: 0.85,
provider: async ({ selector, candidates }) => {
const match = candidates.find((candidate) => candidate.testId === "submit-order");
return match
? { selector: match.selector, confidence: 0.96, reason: `Repaired ${selector} using data-testid` }
: undefined;
},
onRepair: (request, result) => console.log(request.selector, "=>", result.selector)
}
});The provider can call an external AI service or a local model. Repair is opt-in, low-confidence suggestions are rejected, and the original and repaired selectors can be recorded through onRepair.
Quick start
import { withPage } from "nordweb";
const heading = await withPage(async (page) => {
await page.goto("https://example.com");
return page.textContent("h1");
});
console.log(heading);The current direct-CDP core supports navigation, JavaScript evaluation, title and text lookup, CSS clicks, form filling, and PNG screenshots. It intentionally avoids Playwright's broad abstraction and auto-waiting overhead. Add CHROME_PATH or pass executablePath when Chrome is not on PATH.
Locators
Create a reusable CSS locator and use it for common DOM actions:
const save = page.locator("#save");
await save.waitFor();
await save.click();
console.log(await save.textContent());
console.log(await save.isVisible());
console.log(await save.count());
await page.locator("#email").fill("[email protected]");Available locator methods are count(), textContent(), click(), doubleClick(), hover(), fill(value), selectOption(value), check(), uncheck(), inputValue(), getAttribute(name), isVisible(), isEnabled(), and waitFor(timeout). Actions automatically wait up to 5 seconds for the element to exist, become visible, and become enabled. Pass an explicit waitFor(timeout) when you need a different readiness window.
Semantic selectors and locator composition are also supported:
await page.getByRole("button", { name: "Save" }).click();
await page.getByText("Products", { exact: true }).waitFor();
await page.getByLabel("Email address").fill("[email protected]");
await page.getByPlaceholder("Search products").fill("keyboard");
await page.getByTestId("submit-btn").click();
await page.getByAltText("Company logo").waitFor();
await page.getByTitle("Tooltip text").hover();
await page.getById("email").fill("[email protected]");
await page.getByName("username").fill("nordweb");
await page.getByValue("Submit").click();
await page.getByXPath("//button[@type='submit']").click();
const cards = page.locator(".card");
await cards.first().locator("h2").waitFor();
await cards.last().click();
await cards.nth(1).click();Advanced selector methods are getById(id), getByName(name), getByValue(value, options), getByXPath(xpath), getByRole(role, options), getByText(text, options), getByLabel(label, options), getByPlaceholder(placeholder, options), getByTestId(testId), getByAltText(altText, options), getByTitle(title, options), locator(selector), first(), last(), and nth(index). getByText() uses XPath as a fallback when its DOM text query does not find a match.
Enhanced locator options
getByRole(role, { name?, exact?, level?, includeHidden? })- richer accessible-name matching with heading level and hidden element controlgetByLabel(label, { exact?, selector? })- supports custom target selectors for complex formsgetByPlaceholder(placeholder, { exact? })- exact or partial matchinggetByAltText(altText, { exact? })- exact or partial matching for imagesgetByTitle(title, { exact? })- exact or partial matching for title attributes
Keyboard and mouse
Keyboard and mouse input use Chrome DevTools Protocol directly:
await page.locator("#email").focus();
await page.keyboard.type("[email protected]");
await page.keyboard.press("Enter");
await page.mouse.move(200, 120);
await page.mouse.click(200, 120);Keyboard methods are down(key), up(key), press(key), type(text), and insertText(text). Mouse methods are move(x, y), click(x, y), down(), and up().
Navigation events
page.on("load", () => console.log("page loaded"));
await page.goto("https://example.com");
await page.waitForNavigation();
await page.waitForURL("https://example.com/");Supported navigation APIs are on("load" | "domcontentloaded", listener), waitForNavigation(timeout), and waitForURL(url, timeout).
Network interception
Intercept requests with exact URLs, * wildcards, regular expressions, or predicate functions:
await page.route("https://api.example.test/products", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
headers: { "access-control-allow-origin": "*" },
body: JSON.stringify([{ id: 1, name: "NordWeb" }])
});
});
const request = page.waitForRequest(/products/);
const response = page.waitForResponse("https://api.example.test/products");
await page.evaluate("fetch('https://api.example.test/products')");
console.log((await response).status, (await request).method);
await page.unroute();Routes can call continue() with request overrides, fulfill() with a mocked response, or abort() with a CDP network error reason. Use recordHar(path) and stopHar() to capture traffic, then replayHar(path) to serve the recorded responses. Response objects provide body(), text(), and json() helpers.
Network assertions inspect captured traffic and retry until it appears:
await expect(page).toHaveRequest(/products/);
await expect(page).toHaveResponse(/products/, 200);Multiple pages and contexts
const context = await browser.newContext({ baseURL: "https://example.com" });
const first = await context.newPage();
const second = await context.newPage();
await first.goto("/");
await second.goto("/");
console.log(context.pages().length);Contexts provide independent browser storage and page collections. Use browser.contexts() to inspect created contexts.
Browser state and emulation
Configure browser state when creating a context:
const context = await browser.newContext({
baseURL: "https://example.com",
viewport: { width: 1280, height: 720 },
userAgent: "NordWeb CI",
locale: "en-US",
timezoneId: "UTC",
geolocation: { latitude: 40.7, longitude: -74 },
permissions: ["geolocation"]
});
await context.addCookies([{ name: "session", value: "abc", domain: "example.com", path: "/" }]);
console.log(await context.cookies());Save and restore authentication state, including cookies, local storage, and session storage:
await page.goto("/login");
await context.saveStorageState("artifacts/auth.json");
const authenticated = await browser.newContext({
baseURL: "https://example.com",
storageState: "artifacts/auth.json"
});
const pageWithAuth = await authenticated.newPage();
await pageWithAuth.goto("/dashboard");Use context.clearCookies() to reset cookies. page.storageState() and page.restoreStorageState(state) are available for page-level state control. Storage is restored after navigation to its matching origin.
Browser lifecycle
Wait for popups and new pages, downloads, file choosers, and JavaScript dialogs:
const popup = await page.waitForPopup(() => page.getByRole("button", { name: "Open" }).click());
const download = await page.waitForDownload(() => page.getByText("Download").click());
console.log(download.suggestedFilename, await download.path());
const dialog = await page.waitForDialog(() => page.getByRole("button", { name: "Delete" }).click());
await dialog.accept();
const chooser = await page.waitForFileChooser(() => page.locator("input[type=file]").click());
await chooser.setFiles("fixtures/avatar.png");Configure downloadPath on newContext() to control where completed downloads are stored. Use frameLocator(selector).locator(selector) for same-origin iframe content. Regular locators traverse open shadow roots automatically, including composed locators.
NordWeb cleanup is automatic and idempotent: closing a browser closes its contexts and pages, removes active routes and event listeners, stops active screencasts, terminates the Chrome process, deletes its temporary profile, and removes temporary download folders created by NordWeb. User-provided downloadPath directories are preserved.
Assertions
import { expect } from "nordweb";
await expect(page).toHaveTitle("Example Domain");
await expect(page.locator("h1")).toBeVisible();
await expect(page.locator("h1")).toHaveText("Example Domain");
await expect(page.locator("h1")).toHaveCount(1);
await expect(page.locator("#email")).toHaveValue("[email protected]");
await expect(page.locator("#save")).toHaveAttribute("title", "Save");
await expect(page.locator("#save")).toContainText("Save");
await expect(page.locator("#save")).toBeEnabled();
await expect(page.locator("#agree")).toBeChecked();
await expect(page.locator("#save")).toHaveClass("primary");Assertions retry until the condition passes or the default 5-second timeout expires. Each matcher accepts an optional timeout.
Parallel execution and reports
Run independent test callbacks concurrently with isolated browser contexts:
import { runParallel } from "nordweb";
const results = await runParallel([
{ name: "login", run: async (page) => { await page.goto("https://example.com/login"); } },
{ name: "search", run: async (page) => { await page.goto("https://example.com/search"); } }
], {
workers: 2,
artifactDir: "test-results",
trace: true,
screenshotOnFailure: true
});workers limits concurrent tests. Each test receives a fresh context and page. Results include status, duration, error, trace path, and failure screenshot path. Traces are Chrome trace JSON files and screenshots are PNG files.
Better debugging
Capture the full document, screenshots around named steps, browser console output, network activity, and a DOM snapshot:
await page.fullPageScreenshot("artifacts/full-page.png");
await page.step("submit form", async () => {
await page.getByRole("button", { name: "Submit" }).click();
}, "artifacts/steps");
await page.startDebugLogging();
await page.goto("https://example.com");
await page.stopDebugLogging({
consolePath: "artifacts/console.json",
networkPath: "artifacts/network.json"
});
await page.domSnapshot("artifacts/dom.html");Use startTracing() and stopTracing(path) for Chrome trace JSON, or set traceViewer: true in runParallel() to create a self-contained HTML trace report. Set screenshotOnFailure, domSnapshotOnFailure, consoleLogs, and networkLogs in runParallel() to collect failure artifacts automatically.
When a test fails, NordWeb automatically analyzes the failure message and any captured DOM snapshot, screenshot, trace, console log, and network log. The result includes a structured diagnosis with a summary, findings, and evidence list. The HTML reporter displays the diagnosis alongside the test artifacts, while JSON output includes the same data for CI tooling.
Test results also include a readable networkWaterfall with request URL, status, start/end timing, duration, and failure details. The HTML report shows each request and highlights failed responses and failed network loads so slow or failing dependencies are easy to identify.
startVideo(directory) captures a timestamped JPEG screencast frame sequence and writes manifest.json when stopped. This is intentionally dependency-free; use an external encoder such as FFmpeg to convert the sequence to MP4 or WebM.
launch starts headless Chrome with an isolated temporary profile and one CDP WebSocket connection.
Headless and headed modes
Headless mode is the default and is useful for CI:
import { withPage } from "nordweb";
await withPage(async (page) => {
await page.goto("https://example.com");
}, { browser: { headless: true } });Set headless: false to show the Chrome window while developing or debugging locally:
await withPage(async (page) => {
await page.goto("https://example.com");
}, { browser: { headless: false } });Headed mode requires a graphical desktop session. In CI environments, use headless mode unless a display server is configured.
Runtime architecture
NordWeb is developed from scratch for its automation layer. It does not wrap or import Playwright. Node.js runs the client in V8, Chrome runs the application JavaScript in its own V8 instance, and NordWeb sends raw Chrome DevTools Protocol messages over one WebSocket connection. This keeps the transport small and fast while still allowing the browser to execute real DOM code.
V8 alone cannot provide a browser DOM or input devices, so it must be paired with a browser protocol such as CDP. Vitest is only used for testing and is not part of the runtime engine.
