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