Skip to content

Commit cf2f24a

Browse files
committed
Merge branch 'npm-workspace'
2 parents 9426fd2 + 5c30dac commit cf2f24a

5 files changed

Lines changed: 257 additions & 0 deletions

File tree

client/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ backBtn.addEventListener('click', () => {
121121
showView('initial');
122122
channelHash = '';
123123
hashInput.value = '';
124+
userId = '';
124125
});
125126

126127
copyHashBtn.addEventListener('click', () => {

e2e/join-session.spec.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { test, expect, Browser, BrowserContext, Page } from '@playwright/test';
2+
3+
/**
4+
* E2E tests for the two-user join flow.
5+
*
6+
* Regression coverage for: "Key already registered for this session"
7+
* - User A creates a hash and joins.
8+
* - User B joins via that hash.
9+
* - The server previously returned 409 on User B's second sharePublicKey call
10+
* (the one that attaches the encrypted AES key), causing setChannel() to
11+
* throw and the UI to show "Failed to connect."
12+
*/
13+
14+
const APP_URL = 'http://localhost:5173';
15+
16+
async function openUser(browser: Browser): Promise<{ ctx: BrowserContext; page: Page }> {
17+
const ctx = await browser.newContext();
18+
const page = await ctx.newPage();
19+
await page.goto(APP_URL);
20+
// Wait for app to initialise keys
21+
await expect(page.locator('#show-create-hash')).toBeVisible();
22+
return { ctx, page };
23+
}
24+
25+
test.describe('Two-user session join', () => {
26+
test('user B joins after user A without a "Key already registered" error', async ({ browser }) => {
27+
const userA = await openUser(browser);
28+
const userB = await openUser(browser);
29+
30+
// ── User A: create a channel hash ────────────────────────────────────────
31+
await test.step('User A creates a hash', async () => {
32+
await userA.page.click('#show-create-hash');
33+
// Wait until the hash field is populated (not still "Generating...")
34+
await expect(userA.page.locator('#generated-hash-display')).not.toHaveValue('');
35+
await expect(userA.page.locator('#generated-hash-display')).not.toHaveValue('Generating...');
36+
});
37+
38+
const hash = await userA.page.locator('#generated-hash-display').inputValue();
39+
expect(hash.length).toBeGreaterThan(5);
40+
41+
// ── User A: join the channel ──────────────────────────────────────────────
42+
await test.step('User A joins the channel', async () => {
43+
await userA.page.click('#join-btn');
44+
await expect(userA.page.locator('#chat-container')).toBeVisible();
45+
// Must not show an error
46+
await expect(userA.page.locator('#setup-status')).not.toHaveText('Failed to connect.');
47+
});
48+
49+
// ── User B: enter hash and join ───────────────────────────────────────────
50+
await test.step('User B joins using the hash', async () => {
51+
await userB.page.click('#show-join-hash');
52+
await userB.page.fill('#channel-hash', hash);
53+
await userB.page.click('#join-btn');
54+
55+
// Chat container should appear — not the error status
56+
await expect(userB.page.locator('#chat-container')).toBeVisible();
57+
await expect(userB.page.locator('#setup-status')).not.toHaveText('Failed to connect.');
58+
});
59+
60+
// ── Both users should see each other ─────────────────────────────────────
61+
await test.step('User A sees peer joined notification', async () => {
62+
await expect(userA.page.locator('#participant-info')).toHaveText(
63+
'Peer joined. Communication is encrypted.',
64+
);
65+
});
66+
67+
await test.step('User B sees peer already present notification', async () => {
68+
await expect(userB.page.locator('#participant-info')).toHaveText(
69+
'Peer is already here. Communication is encrypted.',
70+
);
71+
});
72+
73+
await userA.ctx.close();
74+
await userB.ctx.close();
75+
});
76+
77+
test('user B can join after going back and retrying (userId reset)', async ({ browser }) => {
78+
const userA = await openUser(browser);
79+
const userB = await openUser(browser);
80+
81+
// User A creates and joins
82+
await userA.page.click('#show-create-hash');
83+
await expect(userA.page.locator('#generated-hash-display')).not.toHaveValue('Generating...');
84+
const hash = await userA.page.locator('#generated-hash-display').inputValue();
85+
await userA.page.click('#join-btn');
86+
await expect(userA.page.locator('#chat-container')).toBeVisible();
87+
88+
// User B: first attempt (enters hash, clicks join)
89+
await userB.page.click('#show-join-hash');
90+
await userB.page.fill('#channel-hash', hash);
91+
await userB.page.click('#join-btn');
92+
await expect(userB.page.locator('#chat-container')).toBeVisible();
93+
94+
await userA.ctx.close();
95+
await userB.ctx.close();
96+
97+
// ── Regression: simulate a fresh user B with a new page (fresh userId) ────
98+
// After page reload userId is cleared; the same hash should be joinable again
99+
// (requires the back-button userId reset fix in client/app.ts).
100+
const userB2 = await openUser(browser);
101+
const userA2 = await openUser(browser);
102+
103+
await userA2.page.click('#show-create-hash');
104+
await expect(userA2.page.locator('#generated-hash-display')).not.toHaveValue('Generating...');
105+
const hash2 = await userA2.page.locator('#generated-hash-display').inputValue();
106+
await userA2.page.click('#join-btn');
107+
await expect(userA2.page.locator('#chat-container')).toBeVisible();
108+
109+
// User B2 tries join → back → retries (simulates back-button userId reset)
110+
await userB2.page.click('#show-join-hash');
111+
await userB2.page.fill('#channel-hash', hash2);
112+
// Go back before joining
113+
await userB2.page.click('#back-btn');
114+
await expect(userB2.page.locator('#initial-actions')).toBeVisible();
115+
// Try again from scratch
116+
await userB2.page.click('#show-join-hash');
117+
await userB2.page.fill('#channel-hash', hash2);
118+
await userB2.page.click('#join-btn');
119+
await expect(userB2.page.locator('#chat-container')).toBeVisible();
120+
await expect(userB2.page.locator('#setup-status')).not.toHaveText('Failed to connect.');
121+
122+
await userA2.ctx.close();
123+
await userB2.ctx.close();
124+
});
125+
126+
test('user A and user B can exchange a message after joining', async ({ browser }) => {
127+
const userA = await openUser(browser);
128+
const userB = await openUser(browser);
129+
130+
// Setup
131+
await userA.page.click('#show-create-hash');
132+
await expect(userA.page.locator('#generated-hash-display')).not.toHaveValue('Generating...');
133+
const hash = await userA.page.locator('#generated-hash-display').inputValue();
134+
await userA.page.click('#join-btn');
135+
await expect(userA.page.locator('#chat-container')).toBeVisible();
136+
137+
await userB.page.click('#show-join-hash');
138+
await userB.page.fill('#channel-hash', hash);
139+
await userB.page.click('#join-btn');
140+
await expect(userB.page.locator('#chat-container')).toBeVisible();
141+
142+
// Wait for both to see each other
143+
await expect(userA.page.locator('#participant-info')).toHaveText(
144+
'Peer joined. Communication is encrypted.',
145+
);
146+
147+
// User A sends a message
148+
await test.step('User A sends a message', async () => {
149+
await userA.page.fill('#msg-input', 'Hello from A');
150+
await userA.page.click('#send-btn');
151+
// Sender sees it immediately in their own list
152+
await expect(userA.page.locator('.message.sent .message-text')).toHaveText('Hello from A');
153+
});
154+
155+
// User B receives it (decrypted)
156+
await test.step('User B receives and decrypts the message', async () => {
157+
await expect(userB.page.locator('.message.received .message-text')).toHaveText('Hello from A');
158+
});
159+
160+
await userA.ctx.close();
161+
await userB.ctx.close();
162+
});
163+
});

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"devDependencies": {
3434
"@commitlint/cli": "^19.5.0",
3535
"@commitlint/config-conventional": "^19.5.0",
36+
"@playwright/test": "^1.59.1",
3637
"@types/cors": "^2.8.17",
3738
"@types/express": "^4.17.21",
3839
"@types/jest": "^29.5.13",

playwright.config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { defineConfig } from '@playwright/test';
2+
3+
export default defineConfig({
4+
testDir: './e2e',
5+
timeout: 30_000,
6+
expect: { timeout: 10_000 },
7+
fullyParallel: false,
8+
retries: 0,
9+
reporter: 'list',
10+
use: {
11+
baseURL: 'http://localhost:5173',
12+
headless: false,
13+
},
14+
webServer: [
15+
{
16+
command: 'npm run serve:dev',
17+
url: 'http://localhost:3001/api',
18+
reuseExistingServer: true,
19+
timeout: 60_000,
20+
},
21+
{
22+
command: 'npm run client:dev',
23+
url: 'http://localhost:5173',
24+
reuseExistingServer: true,
25+
timeout: 60_000,
26+
},
27+
],
28+
});

0 commit comments

Comments
 (0)