diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8d5c925..12739a8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -50,6 +50,11 @@ jobs:
fi
working-directory: "examples/${{ matrix.example }}"
+ - name: Install Playwright browsers
+ run: npx playwright install --with-deps
+ working-directory: "examples/${{ matrix.example }}"
+ if: matrix.example == 'dogfooding'
+
- name: test
run: npm run test --if-present
working-directory: "examples/${{ matrix.example }}"
diff --git a/examples/dogfooding/.gitignore b/examples/dogfooding/.gitignore
new file mode 100644
index 0000000..fb95e4c
--- /dev/null
+++ b/examples/dogfooding/.gitignore
@@ -0,0 +1,13 @@
+# Playwright
+playwright-report/
+test-results/
+playwright/.cache/
+
+# Build output
+build/
+
+# Dependencies
+node_modules/
+
+# OS files
+.DS_Store
\ No newline at end of file
diff --git a/examples/dogfooding/package-lock.json b/examples/dogfooding/package-lock.json
index b05e537..ae561a4 100644
--- a/examples/dogfooding/package-lock.json
+++ b/examples/dogfooding/package-lock.json
@@ -10,18 +10,20 @@
"dependencies": {
"@libp2p/crypto": "^5.0.5",
"@multiformats/multiaddr": "^12.3.1",
- "@waku/sdk": "0.0.32-4997440.0",
+ "@waku/sdk": "0.0.32-b0a2e39.0",
"libp2p": "^2.1.10",
"protobufjs": "^7.3.0",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/interface": "^2.1.3",
+ "@playwright/test": "^1.53.1",
"@types/node": "^20.12.11",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"html-webpack-plugin": "^5.6.3",
+ "playwright": "^1.53.1",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"webpack": "^5.74.0",
@@ -783,6 +785,22 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
+ "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.53.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -1315,16 +1333,16 @@
]
},
"node_modules/@waku/discovery": {
- "version": "0.0.9-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/discovery/-/discovery-0.0.9-4997440.0.tgz",
- "integrity": "sha512-cNJking/6FgHdl0e9x3MC/j7P7jBRn7rF1qU3PPvJ5EiugPcLyrxFRVZocZj4EQzYar7wY909MqNnlA+tGwCaQ==",
+ "version": "0.0.9-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/discovery/-/discovery-0.0.9-b0a2e39.0.tgz",
+ "integrity": "sha512-4dmWAWX16fWecn7xI2WtbuQgoiuHiExF8ch7puv1pTrlLlbrtLm2miWbvgyraacBLUpn12UXUQOuZaM7lMGzGQ==",
"license": "MIT OR Apache-2.0",
"dependencies": {
- "@waku/core": "0.0.36-4997440.0",
- "@waku/enr": "0.0.30-4997440.0",
- "@waku/interfaces": "0.0.31-4997440.0",
- "@waku/proto": "0.0.11-4997440.0",
- "@waku/utils": "0.0.24-4997440.0",
+ "@waku/core": "0.0.36-b0a2e39.0",
+ "@waku/enr": "0.0.30-b0a2e39.0",
+ "@waku/interfaces": "0.0.31-b0a2e39.0",
+ "@waku/proto": "0.0.11-b0a2e39.0",
+ "@waku/utils": "0.0.24-b0a2e39.0",
"debug": "^4.3.4",
"dns-over-http-resolver": "^3.0.8",
"hi-base32": "^0.5.1",
@@ -1335,17 +1353,17 @@
}
},
"node_modules/@waku/discovery/node_modules/@waku/core": {
- "version": "0.0.36-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.36-4997440.0.tgz",
- "integrity": "sha512-1Z8bKLL8fFU7DeeAw+7iDazeNOayudnOSHIT4sP0pxnEe2kpLAaF5C5E0TeRj4Jk43f8xzTKmCMXXwN8/IbFgw==",
+ "version": "0.0.36-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.36-b0a2e39.0.tgz",
+ "integrity": "sha512-c3cXUE45Q5SdwJ9RZXKGhl0cQwu8zkK3GMAfhNoRqMl/qLhRo+Y1u5m45KbG8HasH4Pjpibw8DkMmEjSd0OdRA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/ping": "2.0.1",
"@noble/hashes": "^1.3.2",
- "@waku/enr": "0.0.30-4997440.0",
- "@waku/interfaces": "0.0.31-4997440.0",
- "@waku/proto": "0.0.11-4997440.0",
- "@waku/utils": "0.0.24-4997440.0",
+ "@waku/enr": "0.0.30-b0a2e39.0",
+ "@waku/interfaces": "0.0.31-b0a2e39.0",
+ "@waku/proto": "0.0.11-b0a2e39.0",
+ "@waku/utils": "0.0.24-b0a2e39.0",
"debug": "^4.3.4",
"it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4",
@@ -1419,9 +1437,9 @@
}
},
"node_modules/@waku/enr": {
- "version": "0.0.30-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.30-4997440.0.tgz",
- "integrity": "sha512-LHSJhCFCgEpHuoRWndUlE3JXXKUnHwokL+Av8zSvJPQTbpmSiH/RtFchk4w58muVGQNNzevGkXcNNEZ6/LhtZg==",
+ "version": "0.0.30-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/enr/-/enr-0.0.30-b0a2e39.0.tgz",
+ "integrity": "sha512-6/Uij9zErjUXDUGvmpdMDCCTWngJXkagbGxvNoij0VOfCe6M5wul/VV6qmlepZTbkoTKcv91k3stdrqv1jXaiw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@ethersproject/rlp": "^5.7.0",
@@ -1429,7 +1447,7 @@
"@libp2p/peer-id": "^5.0.1",
"@multiformats/multiaddr": "^12.0.0",
"@noble/secp256k1": "^1.7.1",
- "@waku/utils": "0.0.24-4997440.0",
+ "@waku/utils": "0.0.24-b0a2e39.0",
"debug": "^4.3.4",
"js-sha3": "^0.9.2"
},
@@ -1446,18 +1464,18 @@
}
},
"node_modules/@waku/interfaces": {
- "version": "0.0.31-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.31-4997440.0.tgz",
- "integrity": "sha512-dV0o08C+NDCPTV1xzw6nfikC+1GnBszcY3/Vf2qD6RiZZxNygt9de0N1/c1xgEdGx1eVSrk/XZg8ImNsSl5qLg==",
+ "version": "0.0.31-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.31-b0a2e39.0.tgz",
+ "integrity": "sha512-VXTnq+NA5qLPbwS7nB8hRQjN6D4VY+SUc6eYx5SB2d1MpgBkAyHFD9tKM12yHiIm5N2XFouj5eYzngaMguHF1A==",
"license": "MIT OR Apache-2.0",
"engines": {
"node": ">=20"
}
},
"node_modules/@waku/proto": {
- "version": "0.0.11-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.11-4997440.0.tgz",
- "integrity": "sha512-DkFrWxzryt25mBmeRXwu/h3sR4yMGxRCujDc3fswUW8JssKdCu81QBcOaX6XBy6sda8D8lDdN0399MVY5g3Yyw==",
+ "version": "0.0.11-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/proto/-/proto-0.0.11-b0a2e39.0.tgz",
+ "integrity": "sha512-SQcL5rjSpTUy+1xSjHtq0GLgPd9NGTwCpZRRVu5K7GeN5tVZ5lyVtdXnO7MPVaWW/LYzbc7Fb2QtNxroi8sJ7w==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"protons-runtime": "^5.4.0"
@@ -1467,9 +1485,9 @@
}
},
"node_modules/@waku/sdk": {
- "version": "0.0.32-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/sdk/-/sdk-0.0.32-4997440.0.tgz",
- "integrity": "sha512-3PMJYOUSj6PNL/JGqsRBvKxd4n8RltCKXJ13S97h4x3iGUR5CCYtN5vXa71ePUIL+wtXEhoGVpOZVsgKrZrXuw==",
+ "version": "0.0.32-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/sdk/-/sdk-0.0.32-b0a2e39.0.tgz",
+ "integrity": "sha512-UFoWy9PqtsbUz30qfnvAjrM+uVgCm5NWuzXCl6jyHdgyxplmUFkAfCHLoMTG9SG/ORiC5z07Jru+kOJoNtY5Eg==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@chainsafe/libp2p-noise": "16.0.0",
@@ -1479,11 +1497,11 @@
"@libp2p/ping": "2.0.1",
"@libp2p/websockets": "^9.0.1",
"@noble/hashes": "^1.3.3",
- "@waku/core": "0.0.36-4997440.0",
- "@waku/discovery": "0.0.9-4997440.0",
- "@waku/interfaces": "0.0.31-4997440.0",
- "@waku/proto": "0.0.11-4997440.0",
- "@waku/utils": "0.0.24-4997440.0",
+ "@waku/core": "0.0.36-b0a2e39.0",
+ "@waku/discovery": "0.0.9-b0a2e39.0",
+ "@waku/interfaces": "0.0.31-b0a2e39.0",
+ "@waku/proto": "0.0.11-b0a2e39.0",
+ "@waku/utils": "0.0.24-b0a2e39.0",
"libp2p": "2.1.8"
},
"engines": {
@@ -1491,17 +1509,17 @@
}
},
"node_modules/@waku/sdk/node_modules/@waku/core": {
- "version": "0.0.36-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.36-4997440.0.tgz",
- "integrity": "sha512-1Z8bKLL8fFU7DeeAw+7iDazeNOayudnOSHIT4sP0pxnEe2kpLAaF5C5E0TeRj4Jk43f8xzTKmCMXXwN8/IbFgw==",
+ "version": "0.0.36-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/core/-/core-0.0.36-b0a2e39.0.tgz",
+ "integrity": "sha512-c3cXUE45Q5SdwJ9RZXKGhl0cQwu8zkK3GMAfhNoRqMl/qLhRo+Y1u5m45KbG8HasH4Pjpibw8DkMmEjSd0OdRA==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@libp2p/ping": "2.0.1",
"@noble/hashes": "^1.3.2",
- "@waku/enr": "0.0.30-4997440.0",
- "@waku/interfaces": "0.0.31-4997440.0",
- "@waku/proto": "0.0.11-4997440.0",
- "@waku/utils": "0.0.24-4997440.0",
+ "@waku/enr": "0.0.30-b0a2e39.0",
+ "@waku/interfaces": "0.0.31-b0a2e39.0",
+ "@waku/proto": "0.0.11-b0a2e39.0",
+ "@waku/utils": "0.0.24-b0a2e39.0",
"debug": "^4.3.4",
"it-all": "^3.0.4",
"it-length-prefixed": "^9.0.4",
@@ -1573,13 +1591,13 @@
}
},
"node_modules/@waku/utils": {
- "version": "0.0.24-4997440.0",
- "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.24-4997440.0.tgz",
- "integrity": "sha512-abWy/tUpJLEEX9lbTIl/1TfDuODc5Og55T5n1vtzrb7xTPdaBxOlx0jbGcQdZO/ZfrunKJEY7Vdb/Ofh4XW/DQ==",
+ "version": "0.0.24-b0a2e39.0",
+ "resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.24-b0a2e39.0.tgz",
+ "integrity": "sha512-p8ZvIhROTrUFX+T2CKgepIypkWcHEMdJaJsIrxTIRb8xC412FiWNGGzViAJZ43rckvUatooEER/dvZ55ANFAyw==",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@noble/hashes": "^1.3.2",
- "@waku/interfaces": "0.0.31-4997440.0",
+ "@waku/interfaces": "0.0.31-b0a2e39.0",
"chai": "^4.3.10",
"debug": "^4.3.4",
"uint8arrays": "^5.0.1"
@@ -3107,9 +3125,9 @@
}
},
"node_modules/dns-over-http-resolver": {
- "version": "3.0.15",
- "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-3.0.15.tgz",
- "integrity": "sha512-h2Ldu6b8LjW725Q5zjjv7T5s1K3dPjlU3DWvcEFqB3Ksb3QmqC4dHhPKlGlBS/1P47D4T5arZMiE4dD4OIfO6A==",
+ "version": "3.0.16",
+ "resolved": "https://registry.npmjs.org/dns-over-http-resolver/-/dns-over-http-resolver-3.0.16.tgz",
+ "integrity": "sha512-Qnq8HhNRuMnA61pf1lVPlStCAv1BVrraCx0umPESWgYKf995tUMF5oNhW59PKdnf7E8d5yqwHlEoFywXjsNMCw==",
"license": "Apache-2.0 OR MIT",
"dependencies": {
"quick-lru": "^7.0.0",
@@ -7167,6 +7185,53 @@
"node": ">=8"
}
},
+ "node_modules/playwright": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
+ "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.53.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
+ "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/possible-typed-array-names": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
diff --git a/examples/dogfooding/package.json b/examples/dogfooding/package.json
index ce70105..27febca 100644
--- a/examples/dogfooding/package.json
+++ b/examples/dogfooding/package.json
@@ -4,23 +4,27 @@
"private": true,
"scripts": {
"build": "NODE_ENV=production webpack --config webpack.config.js --mode production",
- "start": "webpack-dev-server"
+ "start": "webpack-dev-server",
+ "test": "playwright test",
+ "test:ui": "playwright test --ui"
},
"dependencies": {
"@libp2p/crypto": "^5.0.5",
"@multiformats/multiaddr": "^12.3.1",
- "@waku/sdk": "0.0.32-4997440.0",
+ "@waku/sdk": "0.0.32-b0a2e39.0",
"libp2p": "^2.1.10",
"protobufjs": "^7.3.0",
"uint8arrays": "^5.1.0"
},
"devDependencies": {
"@libp2p/interface": "^2.1.3",
+ "@playwright/test": "^1.53.1",
"@types/node": "^20.12.11",
"copy-webpack-plugin": "^11.0.0",
"eslint": "^8",
"eslint-config-next": "13.5.6",
"html-webpack-plugin": "^5.6.3",
+ "playwright": "^1.53.1",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"webpack": "^5.74.0",
diff --git a/examples/dogfooding/playwright-report/index.html b/examples/dogfooding/playwright-report/index.html
new file mode 100644
index 0000000..c8337b4
--- /dev/null
+++ b/examples/dogfooding/playwright-report/index.html
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+ Playwright Test Report
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/dogfooding/playwright.config.ts b/examples/dogfooding/playwright.config.ts
new file mode 100644
index 0000000..40b7005
--- /dev/null
+++ b/examples/dogfooding/playwright.config.ts
@@ -0,0 +1,27 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: true,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: process.env.CI ? 1 : undefined,
+ reporter: 'html',
+ use: {
+ baseURL: 'http://localhost:8080',
+ trace: 'on-first-retry',
+ },
+
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+ ],
+
+ webServer: {
+ command: 'npm start',
+ port: 8080,
+ reuseExistingServer: !process.env.CI,
+ },
+});
\ No newline at end of file
diff --git a/examples/dogfooding/src/light-push-errors.ts b/examples/dogfooding/src/light-push-errors.ts
new file mode 100644
index 0000000..7b70729
--- /dev/null
+++ b/examples/dogfooding/src/light-push-errors.ts
@@ -0,0 +1,17 @@
+// Light Push V3 Error Code Mapping
+export const LIGHT_PUSH_V3_ERRORS: Record = {
+ 'not_published_to_any_peer': 'Message was not relayed to any peers. This can happen if the remote peer has no relay connections.',
+ 'rate_limited': 'Message rejected due to rate limiting. Please slow down message sending.',
+ 'bad_request': 'Invalid message format or parameters.',
+ 'internal_server_error': 'Remote peer encountered an internal error.',
+ 'no_peers_available': 'No suitable peers found for relaying the message.',
+ 'duplicate_message': 'Message already exists in the network.',
+ 'message_too_large': 'Message exceeds the maximum allowed size.',
+ 'invalid_topic': 'The pubsub topic or content topic is invalid.',
+ 'unauthorized': 'Not authorized to publish to this topic.',
+ 'service_unavailable': 'Light Push service temporarily unavailable.'
+};
+
+export function getLightPushErrorMessage(error: string): string {
+ return LIGHT_PUSH_V3_ERRORS[error] || `Unknown Light Push error: ${error}`;
+}
\ No newline at end of file
diff --git a/examples/dogfooding/tests/app.spec.ts b/examples/dogfooding/tests/app.spec.ts
new file mode 100644
index 0000000..2843b14
--- /dev/null
+++ b/examples/dogfooding/tests/app.spec.ts
@@ -0,0 +1,84 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Waku Dogfooding App', () => {
+ test('should load the app and initialize Waku node', async ({ page }) => {
+ // Navigate to the app
+ await page.goto('/');
+
+ // Wait for the app to load
+ await expect(page).toHaveTitle(/Waku/);
+
+ // Wait for Waku node initialization
+ await page.waitForFunction(() => {
+ return (window as any).waku !== undefined;
+ }, { timeout: 30000 });
+
+ // Verify Waku node is available and has expected properties
+ const wakuNodeInfo = await page.evaluate(() => {
+ const waku = (window as any).waku;
+ if (!waku) return null;
+
+ return {
+ isStarted: typeof waku.isStarted === 'function' ? waku.isStarted() : false,
+ peerId: waku.peerId?.toString() || null,
+ hasLightPush: !!waku.lightPush,
+ hasFilter: !!waku.filter,
+ hasStore: !!waku.store,
+ };
+ });
+
+ // Assert Waku node is properly initialized
+ expect(wakuNodeInfo).not.toBeNull();
+ expect(wakuNodeInfo?.isStarted).toBe(true);
+ expect(wakuNodeInfo?.peerId).toBeTruthy();
+ expect(wakuNodeInfo?.hasLightPush).toBe(true);
+ expect(wakuNodeInfo?.hasFilter).toBe(true);
+ expect(wakuNodeInfo?.hasStore).toBe(true);
+
+ // Verify UI elements are present
+ await expect(page.locator('#peerIdDisplay')).toBeVisible();
+ await expect(page.locator('#peerIdDisplay')).not.toHaveText('Connecting...');
+ // Peer IDs can start with either 16Uiu2 or 12D3KooW depending on the key type
+ const peerIdText = await page.locator('#peerIdDisplay').textContent();
+ expect(peerIdText).toMatch(/^(16Uiu2|12D3KooW)/);
+
+ // Verify send message button is present
+ await expect(page.locator('#sendMessageButton')).toBeVisible();
+ });
+
+ test('should display peer ID in the UI', async ({ page }) => {
+ await page.goto('/');
+
+ // Wait for peer ID to be displayed
+ await page.waitForSelector('#peerIdDisplay', { state: 'visible' });
+
+ // Wait for the actual peer ID to load (not "Connecting...")
+ await page.waitForFunction(() => {
+ const el = document.querySelector('#peerIdDisplay');
+ return el && el.textContent !== 'Connecting...';
+ }, { timeout: 30000 });
+
+ const peerIdText = await page.locator('#peerIdDisplay').textContent();
+ expect(peerIdText).toBeTruthy();
+ expect(peerIdText).toMatch(/^(16Uiu2|12D3KooW)/); // Peer IDs can start with either prefix
+ });
+
+ test('should have functional message sending UI', async ({ page }) => {
+ await page.goto('/');
+
+ // Wait for Waku node to be ready
+ await page.waitForFunction(() => {
+ return (window as any).waku !== undefined;
+ }, { timeout: 30000 });
+
+ // Check counters are initialized
+ await expect(page.locator('#sentByMeCount')).toHaveText('0');
+ await expect(page.locator('#receivedMineCount')).toHaveText('0');
+ await expect(page.locator('#receivedOthersCount')).toHaveText('0');
+ await expect(page.locator('#failedToSendCount')).toHaveText('0');
+
+ // Verify send button is enabled
+ const sendButton = page.locator('#sendMessageButton');
+ await expect(sendButton).toBeEnabled();
+ });
+});
\ No newline at end of file
diff --git a/examples/dogfooding/tests/lightpush.spec.ts b/examples/dogfooding/tests/lightpush.spec.ts
new file mode 100644
index 0000000..e5c3368
--- /dev/null
+++ b/examples/dogfooding/tests/lightpush.spec.ts
@@ -0,0 +1,151 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Light Push Messages', () => {
+ test('should send 5 messages over 30 seconds with at least one success', async ({ page }) => {
+ test.setTimeout(60000); // Set timeout to 60 seconds for this test
+
+ // Navigate to the app
+ await page.goto('/');
+
+ // Wait for Waku node initialization
+ await page.waitForFunction(() => {
+ return (window as any).waku !== undefined;
+ }, { timeout: 30000 });
+
+ // Wait for peer ID to be displayed (indicates node is ready)
+ await page.waitForSelector('#peerIdDisplay', { state: 'visible' });
+ await page.waitForFunction(() => {
+ const el = document.querySelector('#peerIdDisplay');
+ return el && el.textContent !== 'Connecting...';
+ }, { timeout: 30000 });
+
+ // Remove webpack dev server overlay if it exists
+ await page.evaluate(() => {
+ const overlay = document.querySelector('#webpack-dev-server-client-overlay');
+ if (overlay) {
+ overlay.remove();
+ }
+ });
+
+ // Get initial counter values
+ const getCounters = async () => {
+ return await page.evaluate(() => {
+ return {
+ sent: parseInt(document.querySelector('#sentByMeCount')?.textContent || '0'),
+ receivedMine: parseInt(document.querySelector('#receivedMineCount')?.textContent || '0'),
+ receivedOthers: parseInt(document.querySelector('#receivedOthersCount')?.textContent || '0'),
+ failed: parseInt(document.querySelector('#failedToSendCount')?.textContent || '0')
+ };
+ });
+ };
+
+ const initialCounters = await getCounters();
+ console.log('Initial counters:', initialCounters);
+
+ // Send 5 messages over 30 seconds (one every 6 seconds)
+ const sendButton = page.locator('#sendMessageButton');
+ const messagesPerBatch = 5; // Based on NUM_MESSAGES_PER_BATCH in the app
+ let totalMessagesSent = 0;
+
+ for (let i = 0; i < 5; i++) {
+ console.log(`Sending batch ${i + 1} of 5...`);
+
+ // Click send button (use force if needed to bypass any overlays)
+ await sendButton.click({ force: true });
+ totalMessagesSent += messagesPerBatch;
+
+ // Wait 6 seconds before next batch (except for the last one)
+ if (i < 4) {
+ await page.waitForTimeout(6000);
+ }
+ }
+
+ // Wait a bit for the last messages to be processed
+ await page.waitForTimeout(3000);
+
+ // Get final counter values
+ const finalCounters = await getCounters();
+ console.log('Final counters:', finalCounters);
+
+ // Calculate the changes
+ const sentMessages = finalCounters.sent - initialCounters.sent;
+ const receivedMine = finalCounters.receivedMine - initialCounters.receivedMine;
+ const failedMessages = finalCounters.failed - initialCounters.failed;
+ const totalProcessed = sentMessages + failedMessages;
+
+ console.log(`Messages sent successfully (according to lightPush): ${sentMessages}`);
+ console.log(`Messages received back (mine): ${receivedMine}`);
+ console.log(`Messages failed: ${failedMessages}`);
+ console.log(`Total messages processed: ${totalProcessed}`);
+ console.log(`Total messages expected: ${totalMessagesSent}`);
+
+ // Verify at least one message was successfully delivered
+ // A message is considered successful if either:
+ // 1. Light push reports success (sentMessages > 0), OR
+ // 2. We received our own messages back via Filter (receivedMine > 0)
+ const successfulDeliveries = sentMessages + receivedMine;
+ expect(successfulDeliveries).toBeGreaterThan(0);
+
+ // Verify that at least 20 out of 25 messages were processed (either sent or failed)
+ // Allowing for some messages to be lost due to timing or network issues
+ expect(totalProcessed).toBeGreaterThanOrEqual(20);
+
+ // Additional verification: check message log
+ const allMessageElements = await page.locator('.message-item').count();
+ console.log(`Total messages in UI: ${allMessageElements}`);
+ expect(allMessageElements).toBeGreaterThan(0);
+
+ // Log success rate based on actual delivery
+ const effectiveSuccessRate = (receivedMine / totalMessagesSent) * 100;
+ console.log(`Effective delivery rate: ${effectiveSuccessRate.toFixed(2)}%`);
+ });
+
+ test('should handle message failures gracefully', async ({ page }) => {
+ // Navigate to the app
+ await page.goto('/');
+
+ // Wait for Waku node initialization
+ await page.waitForFunction(() => {
+ return (window as any).waku !== undefined;
+ }, { timeout: 30000 });
+
+ // Remove webpack dev server overlay if it exists
+ await page.evaluate(() => {
+ const overlay = document.querySelector('#webpack-dev-server-client-overlay');
+ if (overlay) {
+ overlay.remove();
+ }
+ });
+
+ // Monitor console for error messages
+ const consoleErrors: string[] = [];
+ page.on('console', (msg) => {
+ if (msg.type() === 'error' || msg.type() === 'warning') {
+ consoleErrors.push(msg.text());
+ }
+ });
+
+ // Send a batch of messages
+ const sendButton = page.locator('#sendMessageButton');
+ await sendButton.click({ force: true });
+
+ // Wait for processing
+ await page.waitForTimeout(3000);
+
+ // Check if failed counter is visible and functional
+ const failedCount = await page.locator('#failedToSendCount').textContent();
+ expect(failedCount).toBeDefined();
+
+ // If there were failures, verify they were logged properly
+ const failedMessages = parseInt(failedCount || '0');
+ if (failedMessages > 0) {
+ // Check for failed messages in the UI
+ const failedMessageElements = await page.locator('.message-item.failed').count();
+ expect(failedMessageElements).toBe(failedMessages);
+
+ // Verify error details are displayed
+ const firstFailedMessage = page.locator('.message-item.failed').first();
+ await expect(firstFailedMessage).toContainText('Failed');
+ }
+ });
+});
\ No newline at end of file
diff --git a/examples/dogfooding/tests/multi-node.spec.ts b/examples/dogfooding/tests/multi-node.spec.ts
new file mode 100644
index 0000000..d8facf5
--- /dev/null
+++ b/examples/dogfooding/tests/multi-node.spec.ts
@@ -0,0 +1,234 @@
+import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
+
+test.describe('Multi-Node Light Push Messages', () => {
+ test('should send messages between two nodes and track delivery', async ({ browser }) => {
+ test.setTimeout(90000); // Set timeout to 90 seconds for this test
+
+ // Create two separate browser contexts (like incognito windows)
+ const context1 = await browser.newContext();
+ const context2 = await browser.newContext();
+
+ // Create pages in each context
+ const page1 = await context1.newPage();
+ const page2 = await context2.newPage();
+
+ // Helper function to initialize a node
+ const initializeNode = async (page: Page, nodeName: string) => {
+ await page.goto('/');
+
+ // Wait for Waku node initialization
+ await page.waitForFunction(() => {
+ return (window as any).waku !== undefined;
+ }, { timeout: 30000 });
+
+ // Wait for peer ID to be displayed
+ await page.waitForSelector('#peerIdDisplay', { state: 'visible' });
+ await page.waitForFunction(() => {
+ const el = document.querySelector('#peerIdDisplay');
+ return el && el.textContent !== 'Connecting...';
+ }, { timeout: 30000 });
+
+ // Remove webpack dev server overlay if it exists
+ await page.evaluate(() => {
+ const overlay = document.querySelector('#webpack-dev-server-client-overlay');
+ if (overlay) {
+ overlay.remove();
+ }
+ });
+
+ // Get peer ID
+ const peerId = await page.locator('#peerIdDisplay').textContent();
+ console.log(`${nodeName} initialized with Peer ID: ${peerId}`);
+
+ return peerId;
+ };
+
+ // Initialize both nodes
+ console.log('Initializing Node 1...');
+ const peerId1 = await initializeNode(page1, 'Node 1');
+
+ console.log('Initializing Node 2...');
+ const peerId2 = await initializeNode(page2, 'Node 2');
+
+ // Helper function to get counters
+ const getCounters = async (page: Page) => {
+ return await page.evaluate(() => {
+ return {
+ sent: parseInt(document.querySelector('#sentByMeCount')?.textContent || '0'),
+ receivedMine: parseInt(document.querySelector('#receivedMineCount')?.textContent || '0'),
+ receivedOthers: parseInt(document.querySelector('#receivedOthersCount')?.textContent || '0'),
+ failed: parseInt(document.querySelector('#failedToSendCount')?.textContent || '0')
+ };
+ });
+ };
+
+ // Get initial counters for both nodes
+ const initialCounters1 = await getCounters(page1);
+ const initialCounters2 = await getCounters(page2);
+
+ console.log('Initial counters Node 1:', initialCounters1);
+ console.log('Initial counters Node 2:', initialCounters2);
+
+ // Send 5 batches of messages from Node 1
+ console.log('\n--- Starting message sending from Node 1 ---');
+ const sendButton1 = page1.locator('#sendMessageButton');
+ const messagesPerBatch = 5;
+ const totalBatches = 5;
+
+ for (let i = 0; i < totalBatches; i++) {
+ console.log(`Node 1: Sending batch ${i + 1} of ${totalBatches}...`);
+ await sendButton1.click({ force: true });
+
+ // Wait between batches
+ if (i < totalBatches - 1) {
+ await page1.waitForTimeout(3000);
+ }
+ }
+
+ // Wait for messages to propagate
+ console.log('Waiting for message propagation...');
+ await page1.waitForTimeout(5000);
+
+ // Get final counters for both nodes
+ const finalCounters1 = await getCounters(page1);
+ const finalCounters2 = await getCounters(page2);
+
+ console.log('\n--- Final Results ---');
+ console.log('Final counters Node 1:', finalCounters1);
+ console.log('Final counters Node 2:', finalCounters2);
+
+ // Calculate results for Node 1 (sender)
+ const node1Results = {
+ sent: finalCounters1.sent - initialCounters1.sent,
+ receivedMine: finalCounters1.receivedMine - initialCounters1.receivedMine,
+ receivedOthers: finalCounters1.receivedOthers - initialCounters1.receivedOthers,
+ failed: finalCounters1.failed - initialCounters1.failed
+ };
+
+ // Calculate results for Node 2 (receiver)
+ const node2Results = {
+ sent: finalCounters2.sent - initialCounters2.sent,
+ receivedMine: finalCounters2.receivedMine - initialCounters2.receivedMine,
+ receivedOthers: finalCounters2.receivedOthers - initialCounters2.receivedOthers,
+ failed: finalCounters2.failed - initialCounters2.failed
+ };
+
+ // Generate report
+ console.log('\n========== DELIVERY REPORT ==========');
+ console.log(`Total messages sent: ${messagesPerBatch * totalBatches}`);
+ console.log('\nNode 1 (Sender):');
+ console.log(` - Peer ID: ${peerId1}`);
+ console.log(` - Messages sent successfully: ${node1Results.sent}`);
+ console.log(` - Messages failed: ${node1Results.failed}`);
+ console.log(` - Own messages received back: ${node1Results.receivedMine}`);
+ console.log(` - Messages from others: ${node1Results.receivedOthers}`);
+
+ console.log('\nNode 2 (Receiver):');
+ console.log(` - Peer ID: ${peerId2}`);
+ console.log(` - Messages received from Node 1: ${node2Results.receivedOthers}`);
+ console.log(` - Own messages received: ${node2Results.receivedMine}`);
+
+ const totalExpected = messagesPerBatch * totalBatches;
+ const node1DeliveryRate = (node1Results.receivedMine / totalExpected) * 100;
+ const node2DeliveryRate = (node2Results.receivedOthers / totalExpected) * 100;
+
+ console.log('\nDelivery Rates:');
+ console.log(` - Node 1 self-delivery rate: ${node1DeliveryRate.toFixed(2)}%`);
+ console.log(` - Node 2 reception rate: ${node2DeliveryRate.toFixed(2)}%`);
+ console.log('=====================================\n');
+
+ // Verify at least one message was delivered
+ expect(node1Results.receivedMine + node2Results.receivedOthers).toBeGreaterThan(0);
+
+ // Verify Node 2 received at least some messages from Node 1
+ expect(node2Results.receivedOthers).toBeGreaterThan(0);
+
+ // Check message elements in UI
+ const messageCount1 = await page1.locator('.message-item').count();
+ const messageCount2 = await page2.locator('.message-item').count();
+
+ console.log(`Messages in Node 1 UI: ${messageCount1}`);
+ console.log(`Messages in Node 2 UI: ${messageCount2}`);
+
+ // Cleanup
+ await context1.close();
+ await context2.close();
+ });
+
+ test('should handle bidirectional messaging between two nodes', async ({ browser }) => {
+ test.setTimeout(90000);
+
+ const context1 = await browser.newContext();
+ const context2 = await browser.newContext();
+ const page1 = await context1.newPage();
+ const page2 = await context2.newPage();
+
+ // Initialize both nodes
+ const initializeNode = async (page: Page, nodeName: string) => {
+ await page.goto('/');
+ await page.waitForFunction(() => (window as any).waku !== undefined, { timeout: 30000 });
+ await page.waitForSelector('#peerIdDisplay', { state: 'visible' });
+ await page.waitForFunction(() => {
+ const el = document.querySelector('#peerIdDisplay');
+ return el && el.textContent !== 'Connecting...';
+ }, { timeout: 30000 });
+ await page.evaluate(() => {
+ const overlay = document.querySelector('#webpack-dev-server-client-overlay');
+ if (overlay) overlay.remove();
+ });
+ const peerId = await page.locator('#peerIdDisplay').textContent();
+ console.log(`${nodeName} initialized with Peer ID: ${peerId}`);
+ return peerId;
+ };
+
+ await initializeNode(page1, 'Node 1');
+ await initializeNode(page2, 'Node 2');
+
+ // Helper to get message counts
+ const getMessageCounts = async (page: Page) => {
+ return await page.evaluate(() => {
+ return {
+ receivedOthers: parseInt(document.querySelector('#receivedOthersCount')?.textContent || '0')
+ };
+ });
+ };
+
+ const initial1 = await getMessageCounts(page1);
+ const initial2 = await getMessageCounts(page2);
+
+ // Send messages from both nodes alternately
+ console.log('\n--- Bidirectional messaging test ---');
+ const sendButton1 = page1.locator('#sendMessageButton');
+ const sendButton2 = page2.locator('#sendMessageButton');
+
+ for (let i = 0; i < 3; i++) {
+ console.log(`Round ${i + 1}: Node 1 sending...`);
+ await sendButton1.click({ force: true });
+ await page1.waitForTimeout(2000);
+
+ console.log(`Round ${i + 1}: Node 2 sending...`);
+ await sendButton2.click({ force: true });
+ await page2.waitForTimeout(2000);
+ }
+
+ // Wait for final propagation
+ await page1.waitForTimeout(3000);
+
+ const final1 = await getMessageCounts(page1);
+ const final2 = await getMessageCounts(page2);
+
+ const node1ReceivedFromNode2 = final1.receivedOthers - initial1.receivedOthers;
+ const node2ReceivedFromNode1 = final2.receivedOthers - initial2.receivedOthers;
+
+ console.log('\n--- Bidirectional Results ---');
+ console.log(`Node 1 received ${node1ReceivedFromNode2} messages from Node 2`);
+ console.log(`Node 2 received ${node2ReceivedFromNode1} messages from Node 1`);
+
+ // Verify bidirectional communication
+ expect(node1ReceivedFromNode2).toBeGreaterThan(0);
+ expect(node2ReceivedFromNode1).toBeGreaterThan(0);
+
+ await context1.close();
+ await context2.close();
+ });
+});
\ No newline at end of file