npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

nordweb

v1.0.3

Published

A low-overhead TypeScript web UI automation API powered by Chrome DevTools Protocol.

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 test

Run NordWeb test files directly with the CLI:

npm run build
nordweb test
nordweb test --grep login --workers 2 --retries 1 --timeout 15000 --reporter dot

After installing NordWeb from npm, the same commands are available through npx:

npx nordweb init
npx nordweb test
npx nordweb test --reporter html

npx 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 offline

For 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 regression

Tests 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 feature

Supported 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.5

Warnings 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 control
  • getByLabel(label, { exact?, selector? }) - supports custom target selectors for complex forms
  • getByPlaceholder(placeholder, { exact? }) - exact or partial matching
  • getByAltText(altText, { exact? }) - exact or partial matching for images
  • getByTitle(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.