Contents

MōBrowser 2.18.0

In this update, we added native modules written in Rust, an end-to-end testing setup for generated projects, and proxy configuration in the Session API. Notifications now work in Windows applications distributed as MSIX packages, and npm run pack no longer leaves half-built artifacts behind when a step fails.

What’s new 

Native modules in Rust 

You can now write the native module of an application in Rust. It uses the same .proto services and the same generated TypeScript code as a C++ module, so the main process calls it, and it calls the main process, the same way as before.

Create a new project with a Rust module, or add one to an existing project:

# A new project with a Rust module and a sample GreetService.
npm create mobrowser-app@latest -- \
  --name App \
  --framework React \
  --library Shadcn \
  --native rust

# Add a Rust module to an existing project.
npm run add -- native --lang rust

For every service, the build generates a trait with an async method per RPC. Implement it and register the implementation in launch():

struct Greeter;

impl GreetService for Greeter {
    async fn say_hello(&self, person: Person) -> Result<String, String> {
        Ok(format!("Hello, {}!", person.name))
    }
}

mobrowser::main!(launch);

fn launch() {
    mobrowser::register_service(GreetServiceServer::new(Greeter));
}

Call it from the main process in the same way as a C++ module:

import { native } from './gen/native';

const response = await native.greet.SayHello({ name: 'John Doe' })
console.log(response.value) // Hello, John Doe!

Service methods run on a Tokio runtime. Returning Err(message) rejects the promise in the main process, and a panic rejects it with panic: <message> while the application keeps running.

The project pins the Rust version in rust-toolchain.toml, and the build installs the toolchain and the Rust target it needs. You don’t need to install protoc. npm run build uses exactly the crate versions in Cargo.lock, and npm run sbom lists the crates linked into the module.

AI skill for E2E testing 

New projects now come with end-to-end tests written with Playwright. A test drives the built application the way a user does: it clicks, types, and checks what the windows show. Every test starts its own instance of the application with a new profile and a hidden window.

Import test and expect from @mobrowser/cli/e2e and put the tests in test/e2e/:

import { expect, test } from "@mobrowser/cli/e2e";

test("greets the user by name", async ({ page }) => {
  await page.getByPlaceholder("Your name").fill("Ada");
  await page.getByRole("button", { name: "Greet" }).click();
  await expect(page.getByText("Hello, Ada!", { exact: true })).toBeVisible();
});

Run them with npm test. Playwright can’t reach native message and file dialogs, so the app fixture answers them instead:

test("deletes the conversation after confirmation", async ({ page, app }) => {
  await app.answerDialog({ button: "Delete" });
  await page.getByRole("button", { name: "Delete conversation" }).click();
  await expect(page.getByRole("listitem")).toHaveCount(0);
});

A failed test keeps a screenshot, a Playwright trace with console messages and network requests, and the output of the application in build/test-results/e2e/. The generated .github/workflows/test.yml runs the tests on macOS, Windows, and Linux for every pull request and uploads the results of failed tests. On Linux without a display, the tests run under Xvfb.

Projects also include the mobrowser-testing skill, which makes AI coding agents keep the checks they make with the automation commands as regression tests.

To add the testing setup to an existing project, run:

npm run add testing

Proxy configuration 

The Session API can now configure how the application reaches the network. Use a fixed proxy server, a PAC script, automatic discovery, the system configuration, or a direct connection:

import { session } from '@mobrowser/api';

session.setProxy({
  mode: 'fixedServers',
  proxyRules: 'proxy.example.com:8080',
  exceptions: 'localhost'
})

// Wait until the new configuration applies to the next load.
await session.forceReloadProxyConfig()

// Find out which proxy a URL goes through.
const proxy = await session.resolveProxy('https://example.com')
console.log(proxy) // PROXY proxy.example.com:8080

session.proxyConfig reports the current configuration. Each mode accepts only its own settings, so a configuration that mixes settings from two modes can’t be written.

A persistent session stores the configuration with its browsing data, so it stays in force after a relaunch even if the application doesn’t call setProxy() again. To return to the default, call setProxy({ mode: 'system' }).

Fixes and improvements 

  • Fixed notifications not being shown on Windows in applications distributed as MSIX packages. Action, selection, and reply callbacks work as well.
  • npm run pack now builds all artifacts in a staging directory and publishes them only after every step succeeds. A failure in vpk, create-dmg, code signing, or notarization no longer leaves half-built installers, release indexes, or a package without an installer in the releases directory, and the next run no longer refuses to pack the same version. --force no longer deletes the previous artifacts before packing, and two pack runs that write to the same directories now fail instead of overwriting each other’s files.
  • Packaged applications on macOS now start one Crashpad handler process instead of two. Crash reports are still collected for the browser and renderer processes.