Skip to content

Commit 56dcd8a

Browse files
author
shijiashuai
committed
fix: improve error handling and expand E2E test coverage
- Add try/catch around JSON.stringify in signaling.js - Add .catch() handlers for void promises - Add "Known Limitations" section to AGENTS.md - Enhance Git Pages with Star CTA and feature highlights - Add E2E tests for call flow, reconnection, and DataChannel - Remove trivial changelog file
1 parent 252d908 commit 56dcd8a

8 files changed

Lines changed: 339 additions & 26 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,12 @@ A change is only complete when:
102102
- automation and local instructions match the actual toolchain
103103
- GitHub metadata matches the site and the implementation
104104
- the OpenSpec change is ready to apply or archive without ambiguity
105+
106+
## Known Limitations
107+
108+
These are intentional design choices, not bugs:
109+
110+
- **No authentication:** This is a demo; production deployments would need auth
111+
- **Public STUN only:** Uses Google's public STUN server; TURN requires manual configuration
112+
- **Chinese UI:** Primary audience is Chinese developers
113+
- **No video recording backend:** Recording is browser-side only (MediaRecorder API)

changelog/archive/2025-02-13_project-infrastructure.md

Lines changed: 0 additions & 22 deletions
This file was deleted.

docs/index.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ description: Documentation hub for LessUp WebRTC: architecture, signaling, deplo
66

77
# Documentation
88

9+
<div class="github-cta" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 24px; margin-bottom: 32px; color: white; text-align: center;">
10+
<h3 style="margin: 0 0 8px 0; color: white;">⭐ Like this project?</h3>
11+
<p style="margin: 0 0 16px 0; opacity: 0.9;">Star us on GitHub to show your support!</p>
12+
<a href="https://github.com/LessUp/webrtc" class="github-button" style="display: inline-block; background: white; color: #333; padding: 10px 24px; border-radius: 6px; text-decoration: none; font-weight: 600;">View on GitHub</a>
13+
</div>
14+
15+
## Why LessUp WebRTC?
16+
17+
| Feature | Description |
18+
|:--------|:------------|
19+
| 🪶 **Lightweight** | Single Go dependency (gorilla/websocket), no heavy frameworks |
20+
|**Zero Build** | Vanilla JavaScript ES6+, served directly—no bundlers, no transpilers |
21+
| 📋 **OpenSpec-Driven** | Spec-first development with structured change management |
22+
| 🌐 **Bilingual Docs** | Complete documentation in English and Chinese |
23+
| 🔧 **Production-Ready** | Docker, Kubernetes, and Fly.io deployment configs included |
24+
925
Use this page as the public entrypoint for understanding the project.
1026

1127
## Start here

docs/index.zh-CN.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ lang: zh-CN
77

88
# 文档首页
99

10+
<div class="github-cta" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; padding: 24px; margin-bottom: 32px; color: white; text-align: center;">
11+
<h3 style="margin: 0 0 8px 0; color: white;">⭐ 喜欢这个项目?</h3>
12+
<p style="margin: 0 0 16px 0; opacity: 0.9;">在 GitHub 上给我们一个 Star 表示支持!</p>
13+
<a href="https://github.com/LessUp/webrtc" class="github-button" style="display: inline-block; background: white; color: #333; padding: 10px 24px; border-radius: 6px; text-decoration: none; font-weight: 600;">访问 GitHub</a>
14+
</div>
15+
16+
## 为什么选择 LessUp WebRTC?
17+
18+
| 特性 | 描述 |
19+
|:-----|:-----|
20+
| 🪶 **轻量级** | 仅一个 Go 依赖 (gorilla/websocket),无重型框架 |
21+
|**零构建** | 原生 JavaScript ES6+,直接服务——无打包器、无转译器 |
22+
| 📋 **OpenSpec 驱动** | 规范优先开发,结构化变更管理 |
23+
| 🌐 **双语文档** | 完整的中英文文档 |
24+
| 🔧 **生产就绪** | 包含 Docker、Kubernetes、Fly.io 部署配置 |
25+
1026
把这里当成公开文档的总入口即可。
1127

1228
## 从这里开始

e2e/call-flow.spec.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('Call flow', function () {
4+
test('two peers can establish a call', async function ({ browser }) {
5+
const context = await browser.newContext();
6+
const pageA = await context.newPage();
7+
const pageB = await context.newPage();
8+
9+
// Both users join the same room
10+
await pageA.goto('/');
11+
await pageA.fill('#room', 'call-test-room');
12+
await pageA.click('#join');
13+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
14+
15+
await pageB.goto('/');
16+
await pageB.fill('#room', 'call-test-room');
17+
await pageB.click('#join');
18+
await expect(pageB.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
19+
20+
// User A calls User B
21+
const memberB = await pageA.locator('.members__item').first().textContent();
22+
if (memberB) {
23+
await pageA.fill('#remote', memberB.trim());
24+
await pageA.click('#call');
25+
26+
// Wait for call to be established
27+
await expect(pageA.locator('#status')).toHaveText(/|calling/i, { timeout: 10000 });
28+
}
29+
30+
await context.close();
31+
});
32+
33+
test('hangup ends the call', async function ({ browser }) {
34+
const context = await browser.newContext();
35+
const pageA = await context.newPage();
36+
const pageB = await context.newPage();
37+
38+
// Setup: both join same room
39+
await pageA.goto('/');
40+
await pageA.fill('#room', 'hangup-test-room');
41+
await pageA.click('#join');
42+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
43+
44+
await pageB.goto('/');
45+
await pageB.fill('#room', 'hangup-test-room');
46+
await pageB.click('#join');
47+
await expect(pageB.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
48+
49+
// Initiate call
50+
const memberB = await pageA.locator('.members__item').first().textContent();
51+
if (memberB) {
52+
await pageA.fill('#remote', memberB.trim());
53+
await pageA.click('#call');
54+
await pageA.waitForTimeout(2000);
55+
}
56+
57+
// Hangup
58+
await pageA.click('#hangup');
59+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
60+
61+
await context.close();
62+
});
63+
});
64+
65+
test.describe('ICE candidate handling', function () {
66+
test('ICE candidates are exchanged during call setup', async function ({ browser }) {
67+
const context = await browser.newContext();
68+
const pageA = await context.newPage();
69+
const pageB = await context.newPage();
70+
71+
// Join room
72+
await pageA.goto('/');
73+
await pageA.fill('#room', 'ice-test-room');
74+
await pageA.click('#join');
75+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
76+
77+
await pageB.goto('/');
78+
await pageB.fill('#room', 'ice-test-room');
79+
await pageB.click('#join');
80+
await expect(pageB.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
81+
82+
// Start call and check for ICE candidate exchange (via connection state)
83+
const memberB = await pageA.locator('.members__item').first().textContent();
84+
if (memberB) {
85+
await pageA.fill('#remote', memberB.trim());
86+
await pageA.click('#call');
87+
88+
// Wait for connection - if ICE works, we should see video elements or calling state
89+
await pageA.waitForTimeout(3000);
90+
91+
// Check that local video is present (media stream obtained)
92+
const localVideo = await pageA.locator('#localVideo');
93+
await expect(localVideo).toBeVisible({ timeout: 5000 });
94+
}
95+
96+
await context.close();
97+
});
98+
});

e2e/datachannel.spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('DataChannel chat', function () {
4+
test('can send and receive messages', async function ({ browser }) {
5+
const context = await browser.newContext();
6+
const pageA = await context.newPage();
7+
const pageB = await context.newPage();
8+
9+
// Both users join the same room
10+
await pageA.goto('/');
11+
await pageA.fill('#room', 'chat-test-room');
12+
await pageA.click('#join');
13+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
14+
15+
await pageB.goto('/');
16+
await pageB.fill('#room', 'chat-test-room');
17+
await pageB.click('#join');
18+
await expect(pageB.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
19+
20+
// Establish call first (required for DataChannel)
21+
const memberB = await pageA.locator('.members__item').first().textContent();
22+
if (memberB) {
23+
await pageA.fill('#remote', memberB.trim());
24+
await pageA.click('#call');
25+
26+
// Wait for call to establish
27+
await pageA.waitForTimeout(3000);
28+
29+
// Check if chat input exists and is visible
30+
const chatInput = pageA.locator('#chatInput');
31+
if (await chatInput.isVisible({ timeout: 2000 }).catch(() => false)) {
32+
// Send a message from A to B
33+
await chatInput.fill('Hello from A!');
34+
await pageA.click('#sendChat');
35+
36+
// Wait for message to arrive
37+
await pageB.waitForTimeout(1000);
38+
39+
// Check if message appears on page B
40+
await expect(pageB.locator('.chat-messages, #messages')).toContainText('Hello from A!', {
41+
timeout: 5000
42+
});
43+
}
44+
}
45+
46+
await context.close();
47+
});
48+
49+
test('chat input is disabled when not in call', async function ({ page }) {
50+
await page.goto('/');
51+
await page.fill('#room', 'chat-disabled-room');
52+
await page.click('#join');
53+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
54+
55+
// Check if chat input is disabled when not in a call
56+
const chatInput = page.locator('#chatInput');
57+
if (await chatInput.isVisible({ timeout: 1000 }).catch(() => false)) {
58+
// Chat input should exist but may be disabled
59+
const isDisabled = await chatInput.isDisabled().catch(() => true);
60+
// If not disabled, the send button might be disabled
61+
const sendButton = page.locator('#sendChat');
62+
const sendDisabled = await sendButton.isDisabled().catch(() => true);
63+
expect(isDisabled || sendDisabled).toBeTruthy();
64+
}
65+
});
66+
});
67+
68+
test.describe('DataChannel availability', function () {
69+
test('DataChannel is available during active call', async function ({ browser }) {
70+
const context = await browser.newContext();
71+
const pageA = await context.newPage();
72+
const pageB = await context.newPage();
73+
74+
// Setup call
75+
await pageA.goto('/');
76+
await pageA.fill('#room', 'dc-avail-room');
77+
await pageA.click('#join');
78+
await expect(pageA.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
79+
80+
await pageB.goto('/');
81+
await pageB.fill('#room', 'dc-avail-room');
82+
await pageB.click('#join');
83+
await expect(pageB.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
84+
85+
// Start call
86+
const memberB = await pageA.locator('.members__item').first().textContent();
87+
if (memberB) {
88+
await pageA.fill('#remote', memberB.trim());
89+
await pageA.click('#call');
90+
91+
// Wait for DataChannel to be ready
92+
await pageA.waitForTimeout(3000);
93+
94+
// Check DataChannel state via console or UI
95+
// If there's a status indicator, check it
96+
const dcStatus = await pageA.locator('#dataChannelStatus, .dc-status').first();
97+
if (await dcStatus.isVisible({ timeout: 1000 }).catch(() => false)) {
98+
await expect(dcStatus).toContainText(/open|ready|connected/i, { timeout: 5000 });
99+
}
100+
}
101+
102+
await context.close();
103+
});
104+
});

e2e/reconnection.spec.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { test, expect } from '@playwright/test';
2+
3+
test.describe('WebSocket reconnection', function () {
4+
test('shows reconnecting state when connection is lost', async function ({ page, context }) {
5+
await page.goto('/');
6+
await page.fill('#room', 'reconnect-test-room');
7+
await page.click('#join');
8+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
9+
10+
// Simulate connection loss by going offline
11+
await context.setOffline(true);
12+
await page.waitForTimeout(1000);
13+
14+
// Should show reconnecting or error state
15+
await expect(page.locator('#error, #status')).toContainText(/|reconnect||disconnect|error/i, {
16+
timeout: 10000
17+
});
18+
19+
// Restore connection
20+
await context.setOffline(false);
21+
});
22+
23+
test('reconnects automatically when connection is restored', async function ({ page, context }) {
24+
await page.goto('/');
25+
await page.fill('#room', 'reconnect-auto-room');
26+
await page.click('#join');
27+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
28+
29+
// Go offline briefly
30+
await context.setOffline(true);
31+
await page.waitForTimeout(2000);
32+
33+
// Restore connection
34+
await context.setOffline(false);
35+
36+
// Should eventually reconnect
37+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 15000 });
38+
});
39+
});
40+
41+
test.describe('Connection state handling', function () {
42+
test('handles page reload gracefully', async function ({ page }) {
43+
await page.goto('/');
44+
await page.fill('#room', 'reload-test-room');
45+
await page.click('#join');
46+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
47+
48+
// Get the user ID
49+
const userId = await page.locator('#myId').textContent();
50+
51+
// Reload the page
52+
await page.reload();
53+
54+
// Should be in idle state after reload
55+
await expect(page.locator('#status')).toHaveText(/|idle/i, { timeout: 5000 });
56+
57+
// Rejoin with same room
58+
await page.fill('#room', 'reload-test-room');
59+
await page.click('#join');
60+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
61+
62+
// Should have a new user ID after reload
63+
const newUserId = await page.locator('#myId').textContent();
64+
expect(newUserId).toBeTruthy();
65+
});
66+
67+
test('handles rapid join/leave cycles', async function ({ page }) {
68+
await page.goto('/');
69+
70+
for (let i = 0; i < 3; i++) {
71+
await page.fill('#room', `rapid-test-room-${i}`);
72+
await page.click('#join');
73+
await expect(page.locator('#status')).toHaveText(/|joined/i, { timeout: 5000 });
74+
75+
await page.click('#join'); // Leave
76+
await expect(page.locator('#status')).toHaveText(/|idle/i, { timeout: 5000 });
77+
}
78+
});
79+
});

0 commit comments

Comments
 (0)