diff --git a/PR.md b/PR.md
deleted file mode 100644
index b53bf78bb3..0000000000
--- a/PR.md
+++ /dev/null
@@ -1,27 +0,0 @@
-### Problem / Description
-- Duplicate browser testing packages increased maintenance and broke CI (referenced removed `@waku/headless-tests`).
-- Dockerized tests failed in CI runners without Docker access.
-
-### Solution
-- Consolidated all browser/headless tests into a single package: `@waku/browser-tests` (removed `packages/headless-tests`).
-- Introduced lightweight bootstrap (`src/assets/bootstrap.js`) and `shared/` module; simplified routes and server.
-- Replaced root-level Dockerfile with a package-local Dockerfile under `packages/browser-tests`.
- - Build image: `cd packages/browser-tests && npm run docker:build`
- - Run dockerized tests: `HEADLESS_USE_CDN_IN_DOCKER=0 npx playwright test tests/docker-server.spec.ts`
-- Fixed Playwright CI to build/test `@waku/browser-tests`; skip Docker-based tests on CI via Playwright `testIgnore`.
-
-### Notes
-- Docker tests require a Docker-enabled environment and local image build; they are intentionally skipped in CI.
-- Resolves: CI failures from removed workspace and duplicated setup.
-- Related to: test infra consolidation and stability.
-
----
-
-#### Checklist
-- [ ] Code changes are **covered by unit tests**.
-- [ ] Code changes are **covered by e2e tests**, if applicable.
-- [ ] **Dogfooding has been performed**, if feasible.
-- [ ] A **test version has been published**, if required.
-- [ ] All **CI checks** pass successfully.
-
-
diff --git a/packages/browser-tests/.dockerignore b/packages/browser-tests/.dockerignore
index 84fafec55b..ddabfcd06a 100644
--- a/packages/browser-tests/.dockerignore
+++ b/packages/browser-tests/.dockerignore
@@ -2,5 +2,3 @@ node_modules
build
.DS_Store
*.log
-# Don't ignore dist - we need the built files
-# dist
diff --git a/packages/browser-tests/README.md b/packages/browser-tests/README.md
index 9069c4c58c..d3ca0a5908 100644
--- a/packages/browser-tests/README.md
+++ b/packages/browser-tests/README.md
@@ -1,178 +1,182 @@
# Waku Browser Tests
-Browser-simulated js-waku node running inside headless Chromium, controlled by an Express server. Useful for long-running simulations and realistic verification in CI/Docker.
+This project provides a system for testing the Waku SDK in a browser environment.
## Architecture
-- **Headless browser**: Playwright launches Chromium and navigates to a static site built from TypeScript and served by the same server, which exposes `window.wakuAPI` and `window.waku`.
-- **Server**: Express app provides REST endpoints and proxies calls into the browser via `page.evaluate(...)`.
-- **Bootstrap module**: Small browser-side module in the static site initializes the API and imports `@waku/sdk`.
-- **Shared code**: `shared/` contains utilities used by tests and for typing.
+The system consists of:
-## Prerequisites
+1. **Headless Web App**: A simple web application (in the `@waku/headless-tests` package) that loads the Waku SDK and exposes shared API functions.
+2. **Express Server**: A server that communicates with the headless app using Playwright.
+3. **Shared API**: TypeScript functions shared between the server and web app.
-- Node.js 18+
-- Playwright (installed via dev dependency)
-- Docker (optional, for Testcontainers-based tests)
+## Setup
-## Install & Build
+1. Install dependencies:
```bash
+# Install main dependencies
npm install
+
+# Install headless app dependencies
+cd ../headless-tests
+npm install
+cd ../browser-tests
+```
+
+2. Build the application:
+
+```bash
npm run build
```
-The build compiles the TypeScript server to `dist/` and bundles the static site to `dist/web/`.
+This will:
+- Build the headless web app using webpack
+- Compile the TypeScript server code
-## Run
+## Running
+
+Start the server with:
```bash
-# Default configuration (cluster ID 1, auto-sharding)
npm run start:server
-
-# Use cluster ID 2 (for 10k sim compatibility)
-npm run start:cluster2
-
-# Use specific cluster and shard
-npm run start:cluster2-shard0
-
-# Or with direct CLI arguments
-npm run build && node dist/src/server.js --cluster-id=2 --shard=3
```
-This starts the API server and a headless browser.
-
-### CLI Arguments
-
-- `--cluster-id=N` - Set the Waku cluster ID (default: 1)
-- `--shard=N` - Set a specific shard for static sharding (0-7, omit for auto-sharding)
-
-## Environment variables
-
-- `PORT`: API server port (default: 8080; Playwright sets this for tests)
-- `WAKU_WS_MULTIADDR`: a single ws/wss multiaddr to dial in tests (overrides peers)
-- `WAKU_WS_MULTIADDRS`: multiple peers as JSON array (e.g. `["/dns4/.../wss/p2p/16U..."]`) or comma-separated string; used when `WAKU_WS_MULTIADDR` is not set
+This will:
+1. Serve the headless app on port 8080
+2. Start a headless browser to load the app
+3. Expose API endpoints to interact with Waku
## API Endpoints
-- `GET /` – health/status
-- `GET /info` – peer info from the node
-- `GET /debug/v1/info` – debug info/protocols
-- `POST /lightpush/v1/message` – push a message (Waku REST-compatible shape)
-- `POST /admin/v1/create-node` – create a node with `networkConfig`
-- `POST /admin/v1/start-node` – start the node
-- `POST /admin/v1/stop-node` – stop the node
-- `POST /admin/v1/peers` – dial to peers
-- `GET /filter/v2/messages/:contentTopic` – SSE subscription to messages
-- `GET /filter/v1/messages/:contentTopic` – retrieve queued messages
-- `POST /execute` – helper to execute functions in the browser context (testing/support)
+- `GET /info`: Get information about the Waku node
+- `GET /debug/v1/info`: Get debug information from the Waku node
+- `POST /push`: Push a message to the Waku network (legacy)
+- `POST /lightpush/v1/message`: Push a message to the Waku network (Waku REST API compatible)
+- `POST /admin/v1/create-node`: Create a new Waku node (requires networkConfig)
+- `POST /admin/v1/start-node`: Start the Waku node
+- `POST /admin/v1/stop-node`: Stop the Waku node
+- `POST /admin/v1/peers`: Dial to specified peers (Waku REST API compatible)
+- `GET /filter/v2/messages/:contentTopic`: Subscribe to messages on a specific content topic using Server-Sent Events (Waku REST API compatible)
+- `GET /filter/v1/messages/:contentTopic`: Retrieve stored messages from a content topic (Waku REST API compatible)
-### Examples
+### Example: Pushing a message with the legacy endpoint
-Push (REST-compatible):
+```bash
+curl -X POST http://localhost:3000/push \
+ -H "Content-Type: application/json" \
+ -d '{"contentTopic": "/toy-chat/2/huilong/proto", "payload": [1, 2, 3]}'
+```
+
+### Example: Pushing a message with the Waku REST API compatible endpoint
```bash
curl -X POST http://localhost:3000/lightpush/v1/message \
-H "Content-Type: application/json" \
-d '{
- "pubsubTopic": "/waku/2/rs/1/0",
+ "pubsubTopic": "/waku/2/rs/0/0",
"message": {
- "payload": [1,2,3],
- "contentTopic": "/test/1/message/proto"
+ "payload": "SGVsbG8sIFdha3Uh",
+ "contentTopic": "/toy-chat/2/huilong/proto",
+ "timestamp": 1712135330213797632
}
}'
```
-Create/Start/Stop:
+### Example: Executing a function
+
+```bash
+curl -X POST http://localhost:3000/execute \
+ -H "Content-Type: application/json" \
+ -d '{"functionName": "getPeerInfo", "params": []}'
+```
+
+### Example: Creating a Waku node
```bash
curl -X POST http://localhost:3000/admin/v1/create-node \
-H "Content-Type: application/json" \
-d '{
"defaultBootstrap": true,
- "networkConfig": { "clusterId": 42, "shards": [0] }
+ "networkConfig": {
+ "clusterId": 1,
+ "shards": [0, 1]
+ }
}'
+```
+### Example: Starting and stopping a Waku node
+
+```bash
+# Start the node
curl -X POST http://localhost:3000/admin/v1/start-node
+
+# Stop the node
curl -X POST http://localhost:3000/admin/v1/stop-node
```
-Dial peers:
+### Example: Dialing to specific peers with the Waku REST API compatible endpoint
```bash
curl -X POST http://localhost:3000/admin/v1/peers \
-H "Content-Type: application/json" \
-d '{
- "peerMultiaddrs": ["/dns4/example/tcp/8000/wss/p2p/16U..."]
+ "peerMultiaddrs": [
+ "/ip4/127.0.0.1/tcp/8000/p2p/16Uiu2HAm4v8KuHUH6Cwz3upPeQbkyxQJsFGPdt7kHtkN8F79QiE6"]
+ ]
}'
```
-SSE subscribe:
+### Example: Dialing to specific peers with the execute endpoint
```bash
-curl -N "http://localhost:3000/filter/v2/messages/test-topic?clusterId=1&shard=0"
+curl -X POST http://localhost:3000/execute \
+ -H "Content-Type: application/json" \
+ -d '{
+ "functionName": "dialPeers",
+ "params": [
+ ["/ip4/127.0.0.1/tcp/8000/p2p/16Uiu2HAm4v8KuHUH6Cwz3upPeQbkyxQJsFGPdt7kHtkN8F79QiE6"]
+ ]
+ }'
```
-Query queued messages:
+### Example: Subscribing to a content topic with the filter endpoint
```bash
-curl "http://localhost:3000/filter/v1/messages/test-topic?pageSize=10&ascending=true"
+# Open a persistent connection to receive messages as Server-Sent Events
+curl -N http://localhost:3000/filter/v2/messages/%2Ftoy-chat%2F2%2Fhuilong%2Fproto
+
+# You can also specify clustering options
+curl -N "http://localhost:3000/filter/v2/messages/%2Ftoy-chat%2F2%2Fhuilong%2Fproto?clusterId=0&shard=0"
```
-## Testing
+### Example: Retrieving stored messages from a content topic
```bash
-npm run build
-npm test
+# Get the most recent 20 messages
+curl http://localhost:3000/filter/v1/messages/%2Ftoy-chat%2F2%2Fhuilong%2Fproto
+
+# Get messages with pagination and time filtering
+curl "http://localhost:3000/filter/v1/messages/%2Ftoy-chat%2F2%2Fhuilong%2Fproto?pageSize=10&startTime=1712000000000&endTime=1713000000000&ascending=true"
```
-Playwright will start the server (uses `npm run start:server`). Ensure the build artifacts exist before running tests.
-
-## Docker Usage
-
-Build and run with default configuration:
-
-```bash
-npm run docker:build
-docker run -p 8080:8080 waku-browser-tests:local
-```
-
-Run with cluster ID 2 for 10k sim compatibility:
-
-```bash
-docker run -p 8080:8080 waku-browser-tests:local --cluster-id=2
-```
-
-Run with specific cluster and shard:
-
-```bash
-docker run -p 8080:8080 waku-browser-tests:local --cluster-id=2 --shard=0
-```
-
-Or using environment variables:
-
-```bash
-docker run -p 8080:8080 -e WAKU_CLUSTER_ID=2 -e WAKU_SHARD=0 waku-browser-tests:local
-```
-
-### Dockerized tests
-
-`tests/docker-server.spec.ts` uses Testcontainers. Ensure Docker is running.
-
-Build the image and run only the docker tests locally:
-
-```bash
-npm run docker:build
-npx playwright test tests/docker-server.spec.ts
-```
-
-Notes:
-- The Docker image runs the server with Playwright Chromium and `--no-sandbox` for container compatibility.
-- Testcontainers will map the container port automatically; the tests probe readiness by waiting for `API server running on http://localhost:` in logs.
-
## Extending
-- To add new REST endpoints: update `src/server.ts` and route handlers.
-- To add new browser-executed functions: prefer updating `src/assets/bootstrap.js` (minimize inline JS in `src/server.ts`).
-- For shared logic usable in tests, add helpers under `shared/`.
+To add new functionality:
+1. Add your function to `src/api/shared.ts`
+2. Add your function to the `API` object in `src/api/shared.ts`
+3. Use it via the server endpoints
+
+### Example: Dialing to specific peers
+
+```bash
+curl -X POST http://localhost:3000/execute \
+ -H "Content-Type: application/json" \
+ -d '{
+ "functionName": "dialPeers",
+ "params": [
+ ["/ip4/127.0.0.1/tcp/8000/p2p/16Uiu2HAm4v8KuHUH6Cwz3upPeQbkyxQJsFGPdt7kHtkN8F79QiE6"]
+ ]
+ }'
+```
diff --git a/packages/browser-tests/scripts/docker-entrypoint.sh b/packages/browser-tests/scripts/docker-entrypoint.sh
index ad2259440f..7a449fa83c 100644
--- a/packages/browser-tests/scripts/docker-entrypoint.sh
+++ b/packages/browser-tests/scripts/docker-entrypoint.sh
@@ -2,6 +2,17 @@
# Docker entrypoint script for waku-browser-tests
# Handles CLI arguments and converts them to environment variables
+# Supports reading discovered addresses from /etc/addrs/addrs.env (10k sim pattern)
+
+# Check if address file exists and source it
+if [ -f "/etc/addrs/addrs.env" ]; then
+ echo "Sourcing discovered addresses from /etc/addrs/addrs.env"
+ source /etc/addrs/addrs.env
+ if [ -n "$addrs1" ]; then
+ export WAKU_LIGHTPUSH_NODE="$addrs1"
+ echo "Using discovered lightpush node: $WAKU_LIGHTPUSH_NODE"
+ fi
+fi
# Parse command line arguments
while [[ $# -gt 0 ]]; do
@@ -16,6 +27,11 @@ while [[ $# -gt 0 ]]; do
echo "Setting WAKU_SHARD=${WAKU_SHARD}"
shift
;;
+ --lightpushnode=*)
+ export WAKU_LIGHTPUSH_NODE="${1#*=}"
+ echo "Setting WAKU_LIGHTPUSH_NODE=${WAKU_LIGHTPUSH_NODE}"
+ shift
+ ;;
*)
# Unknown argument, keep it for the main command
break
diff --git a/packages/browser-tests/src/server.ts b/packages/browser-tests/src/server.ts
index 3a13603c74..120aa6d7b2 100644
--- a/packages/browser-tests/src/server.ts
+++ b/packages/browser-tests/src/server.ts
@@ -40,8 +40,14 @@ app.get("/app/index.html", (_req: Request, res: Response) => {
networkConfig.shards = [parseInt(process.env.WAKU_SHARD, 10)];
}
- // Inject network configuration as a global variable
- const configScript = ` `;
+ // Get lightpushnode configuration from environment
+ const lightpushNode = process.env.WAKU_LIGHTPUSH_NODE || null;
+
+ // Inject network configuration and lightpushnode as global variables
+ const configScript = ` `;
const originalPattern = ' ';
const replacement = `${configScript}\n `;
@@ -174,12 +180,13 @@ process.on("SIGTERM", (async () => {
}) as any);
/**
- * Parse CLI arguments for cluster and shard configuration
+ * Parse CLI arguments for cluster, shard, and lightpushnode configuration
*/
function parseCliArgs() {
const args = process.argv.slice(2);
let clusterId: number | undefined;
let shard: number | undefined;
+ let lightpushNode: string | undefined;
for (const arg of args) {
if (arg.startsWith('--cluster-id=')) {
@@ -194,10 +201,16 @@ function parseCliArgs() {
console.error('Invalid shard value. Must be a number.');
process.exit(1);
}
+ } else if (arg.startsWith('--lightpushnode=')) {
+ lightpushNode = arg.split('=')[1];
+ if (!lightpushNode || lightpushNode.trim() === '') {
+ console.error('Invalid lightpushnode value. Must be a valid multiaddr.');
+ process.exit(1);
+ }
}
}
- return { clusterId, shard };
+ return { clusterId, shard, lightpushNode };
}
const isMainModule = process.argv[1] === fileURLToPath(import.meta.url);
@@ -215,6 +228,10 @@ if (isMainModule) {
process.env.WAKU_SHARD = cliArgs.shard.toString();
console.log(`Using CLI shard: ${cliArgs.shard}`);
}
+ if (cliArgs.lightpushNode !== undefined) {
+ process.env.WAKU_LIGHTPUSH_NODE = cliArgs.lightpushNode;
+ console.log(`Using CLI lightpushnode: ${cliArgs.lightpushNode}`);
+ }
void startServer(port);
}
diff --git a/packages/browser-tests/web/index.ts b/packages/browser-tests/web/index.ts
index d950d34a86..1041423869 100644
--- a/packages/browser-tests/web/index.ts
+++ b/packages/browser-tests/web/index.ts
@@ -38,10 +38,16 @@ function makeSerializable(result: SDKProtocolResult): SerializableSDKProtocolRes
export class WakuHeadless {
waku: LightNode | null;
networkConfig: NetworkConfig;
- constructor(networkConfig?: Partial) {
+ lightpushNode: string | null;
+ constructor(networkConfig?: Partial, lightpushNode?: string) {
this.waku = null as unknown as LightNode;
// Use provided config or defaults
this.networkConfig = this.buildNetworkConfig(networkConfig);
+ this.lightpushNode = lightpushNode || null;
+
+ if (this.lightpushNode) {
+ console.log(`Configured preferred lightpush node: ${this.lightpushNode}`);
+ }
}
/**
@@ -204,11 +210,36 @@ export class WakuHeadless {
const encoder = this.waku.createEncoder({ contentTopic, pubsubTopic });
console.log("Encoder created with pubsubTopic:", encoder.pubsubTopic);
- // Send the message using lightpush
- const result = await lightPush.send(encoder, {
- payload: processedPayload,
- timestamp: new Date(),
- });
+
+ // Send the message using lightpush with preferred peer if configured
+ let result;
+ if (this.lightpushNode) {
+ console.log(`Attempting to send via preferred lightpush node: ${this.lightpushNode}`);
+ try {
+ // Try to send to preferred peer first
+ const preferredPeerId = await this.getPeerIdFromMultiaddr(this.lightpushNode);
+ if (preferredPeerId) {
+ result = await lightPush.send(encoder, {
+ payload: processedPayload,
+ timestamp: new Date(),
+ }, { peerId: preferredPeerId });
+ console.log("✅ Message sent via preferred lightpush node");
+ } else {
+ throw new Error("Could not extract peer ID from preferred node address");
+ }
+ } catch (error) {
+ console.warn("Failed to send via preferred node, falling back to default:", error);
+ result = await lightPush.send(encoder, {
+ payload: processedPayload,
+ timestamp: new Date(),
+ });
+ }
+ } else {
+ result = await lightPush.send(encoder, {
+ payload: processedPayload,
+ timestamp: new Date(),
+ });
+ }
// Convert to serializable format for cross-context communication
const serializableResult = makeSerializable(result);
@@ -332,9 +363,80 @@ export class WakuHeadless {
console.log("Starting Waku node...");
await this.waku.start();
console.log("Waku node started, peer ID:", this.waku.libp2p.peerId.toString());
+
+ // If a preferred lightpush node is configured, dial it
+ if (this.lightpushNode) {
+ await this.dialPreferredLightpushNode();
+ }
+
return { success: true };
}
+ /**
+ * Dial the preferred lightpush node if configured
+ */
+ private async dialPreferredLightpushNode() {
+ if (!this.waku || !this.lightpushNode) {
+ return;
+ }
+
+ try {
+ console.log(`Dialing preferred lightpush node: ${this.lightpushNode}`);
+ await this.waku.dial(this.lightpushNode);
+ console.log(`Successfully connected to preferred lightpush node: ${this.lightpushNode}`);
+ } catch (error) {
+ console.warn(`Failed to dial preferred lightpush node ${this.lightpushNode}:`, error);
+ // Don't throw error - fallback to default peer discovery
+ }
+ }
+
+ /**
+ * Extract peer ID from multiaddr string
+ */
+ private async getPeerIdFromMultiaddr(multiaddr: string): Promise {
+ if (!this.waku) {
+ return null;
+ }
+
+ try {
+ // Check if this peer is already connected
+ const connectedPeers = this.waku.libp2p.getPeers();
+
+ // Try to match by the multiaddr - this is a simplified approach
+ // In a real implementation, you'd parse the multiaddr to extract the peer ID
+ for (const peerId of connectedPeers) {
+ try {
+ const peerInfo = await this.waku.libp2p.peerStore.get(peerId);
+ for (const addr of peerInfo.addresses) {
+ if (addr.multiaddr.toString().includes(multiaddr.split('/')[2])) {
+ console.log(`Found matching peer ID for ${multiaddr}: ${peerId.toString()}`);
+ return peerId;
+ }
+ }
+ } catch (e) {
+ // Continue searching
+ }
+ }
+
+ // If not found, try to extract from multiaddr format
+ // Format: /ip4/x.x.x.x/tcp/port/p2p/peerID
+ const parts = multiaddr.split('/');
+ const p2pIndex = parts.indexOf('p2p');
+ if (p2pIndex !== -1 && p2pIndex + 1 < parts.length) {
+ const peerIdString = parts[p2pIndex + 1];
+ console.log(`Extracted peer ID from multiaddr: ${peerIdString}`);
+ // For now, return as string - the actual implementation might need proper PeerId construction
+ return peerIdString;
+ }
+
+ console.warn(`Could not extract peer ID from multiaddr: ${multiaddr}`);
+ return null;
+ } catch (error) {
+ console.warn("Error extracting peer ID from multiaddr:", error);
+ return null;
+ }
+ }
+
async stopNode() {
if (!this.waku) {
throw new Error("Waku node not created");
@@ -519,7 +621,11 @@ export class WakuHeadless {
// Check for global network configuration set by server
const globalNetworkConfig = (window as any).__WAKU_NETWORK_CONFIG;
- const instance = new WakuHeadless(globalNetworkConfig);
+
+ // Check for global lightpushnode configuration set by server
+ const globalLightpushNode = (window as any).__WAKU_LIGHTPUSH_NODE;
+
+ const instance = new WakuHeadless(globalNetworkConfig, globalLightpushNode);
// @ts-ignore - will add proper typings in global.d.ts
(window as any).wakuApi = instance;