feat: Add user metadata changing display name and description with sync
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
All checks were successful
Queue Release Build / prepare (push) Successful in 28s
Deploy Web Apps / deploy (push) Successful in 5m2s
Queue Release Build / build-windows (push) Successful in 16m44s
Queue Release Build / build-linux (push) Successful in 27m12s
Queue Release Build / finalize (push) Successful in 22s
This commit is contained in:
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
expect: { timeout: 10_000 },
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [['html', { outputFolder: '../test-results/html-report' }], ['list']],
|
||||
reporter: [['html', { outputFolder: '../test-results/html-report', open: 'never' }], ['list']],
|
||||
outputDir: '../test-results/artifacts',
|
||||
use: {
|
||||
baseURL: 'http://localhost:4200',
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
mkdtemp,
|
||||
rm
|
||||
} from 'node:fs/promises';
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
@@ -9,11 +6,9 @@ import {
|
||||
type BrowserContext,
|
||||
type Page
|
||||
} from '@playwright/test';
|
||||
import {
|
||||
test,
|
||||
expect
|
||||
} from '../../fixtures/multi-client';
|
||||
import { test, expect } from '../../fixtures/multi-client';
|
||||
import { installTestServerEndpoint } from '../../helpers/seed-test-endpoint';
|
||||
import { installWebRTCTracking } from '../../helpers/webrtc-helpers';
|
||||
import { LoginPage } from '../../pages/login.page';
|
||||
import { RegisterPage } from '../../pages/register.page';
|
||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||
@@ -40,17 +35,39 @@ interface PersistentClient {
|
||||
userDataDir: string;
|
||||
}
|
||||
|
||||
interface ProfileMetadata {
|
||||
description?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
const STATIC_GIF_BASE64 = 'R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
const GIF_FRAME_MARKER = Buffer.from([0x21, 0xF9, 0x04]);
|
||||
const NETSCAPE_LOOP_EXTENSION = Buffer.from([
|
||||
0x21, 0xFF, 0x0B,
|
||||
0x4E, 0x45, 0x54, 0x53, 0x43, 0x41, 0x50, 0x45, 0x32, 0x2E, 0x30,
|
||||
0x03, 0x01, 0x00, 0x00, 0x00
|
||||
const GIF_FRAME_MARKER = Buffer.from([
|
||||
0x21,
|
||||
0xF9,
|
||||
0x04
|
||||
]);
|
||||
const CLIENT_LAUNCH_ARGS = [
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--use-fake-ui-for-media-stream'
|
||||
];
|
||||
const NETSCAPE_LOOP_EXTENSION = Buffer.from([
|
||||
0x21,
|
||||
0xFF,
|
||||
0x0B,
|
||||
0x4E,
|
||||
0x45,
|
||||
0x54,
|
||||
0x53,
|
||||
0x43,
|
||||
0x41,
|
||||
0x50,
|
||||
0x45,
|
||||
0x32,
|
||||
0x2E,
|
||||
0x30,
|
||||
0x03,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00
|
||||
]);
|
||||
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
|
||||
const VOICE_CHANNEL = 'General';
|
||||
|
||||
test.describe('Profile avatar sync', () => {
|
||||
@@ -100,6 +117,8 @@ test.describe('Profile avatar sync', () => {
|
||||
await joinServerFromSearch(bob.page, serverName);
|
||||
await waitForRoomReady(alice.page);
|
||||
await waitForRoomReady(bob.page);
|
||||
await waitForConnectedPeerCount(alice.page, 1);
|
||||
await waitForConnectedPeerCount(bob.page, 1);
|
||||
await expectUserRowVisible(bob.page, aliceUser.displayName);
|
||||
});
|
||||
|
||||
@@ -126,6 +145,8 @@ test.describe('Profile avatar sync', () => {
|
||||
await registerUser(carol);
|
||||
await joinServerFromSearch(carol.page, serverName);
|
||||
await waitForRoomReady(carol.page);
|
||||
await waitForConnectedPeerCount(alice.page, 2);
|
||||
await waitForConnectedPeerCount(carol.page, 1);
|
||||
|
||||
await expectSidebarAvatar(carol.page, aliceUser.displayName, avatarA.dataUrl);
|
||||
});
|
||||
@@ -177,6 +198,134 @@ test.describe('Profile avatar sync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Profile metadata sync', () => {
|
||||
test.describe.configure({ timeout: 240_000 });
|
||||
|
||||
test('syncs display name and description changes for online and late-joining users and persists after restart', async ({ testServer }) => {
|
||||
const suffix = uniqueName('profile');
|
||||
const serverName = `Profile Sync Server ${suffix}`;
|
||||
const messageText = `Profile sync message ${suffix}`;
|
||||
const firstProfile: ProfileMetadata = {
|
||||
displayName: `Alice One ${suffix}`,
|
||||
description: `First synced profile description ${suffix}`
|
||||
};
|
||||
const secondProfile: ProfileMetadata = {
|
||||
displayName: `Alice Two ${suffix}`,
|
||||
description: `Second synced profile description ${suffix}`
|
||||
};
|
||||
const aliceUser: TestUser = {
|
||||
username: `alice_${suffix}`,
|
||||
displayName: 'Alice',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const bobUser: TestUser = {
|
||||
username: `bob_${suffix}`,
|
||||
displayName: 'Bob',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const carolUser: TestUser = {
|
||||
username: `carol_${suffix}`,
|
||||
displayName: 'Carol',
|
||||
password: 'TestPass123!'
|
||||
};
|
||||
const clients: PersistentClient[] = [];
|
||||
|
||||
try {
|
||||
const alice = await createPersistentClient(aliceUser, testServer.port);
|
||||
const bob = await createPersistentClient(bobUser, testServer.port);
|
||||
|
||||
clients.push(alice, bob);
|
||||
|
||||
await test.step('Alice and Bob register, create a server, and join the same room', async () => {
|
||||
await registerUser(alice);
|
||||
await registerUser(bob);
|
||||
|
||||
const aliceSearchPage = new ServerSearchPage(alice.page);
|
||||
|
||||
await aliceSearchPage.createServer(serverName, {
|
||||
description: 'Profile synchronization E2E coverage'
|
||||
});
|
||||
|
||||
await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
|
||||
await joinServerFromSearch(bob.page, serverName);
|
||||
await waitForRoomReady(alice.page);
|
||||
await waitForRoomReady(bob.page);
|
||||
await waitForConnectedPeerCount(alice.page, 1);
|
||||
await waitForConnectedPeerCount(bob.page, 1);
|
||||
await expectUserRowVisible(bob.page, aliceUser.displayName);
|
||||
});
|
||||
|
||||
const roomUrl = alice.page.url();
|
||||
|
||||
await test.step('Alice updates her profile while Bob is online and Bob sees it live', async () => {
|
||||
await updateProfileFromRoomSidebar(alice.page, {
|
||||
displayName: aliceUser.displayName
|
||||
}, firstProfile);
|
||||
|
||||
await expectUserRowVisible(alice.page, firstProfile.displayName);
|
||||
await expectUserRowVisible(bob.page, firstProfile.displayName);
|
||||
await expectProfileCardDetails(bob.page, firstProfile);
|
||||
});
|
||||
|
||||
const carol = await createPersistentClient(carolUser, testServer.port);
|
||||
|
||||
clients.push(carol);
|
||||
|
||||
await test.step('Carol joins after the first change and sees the updated profile', async () => {
|
||||
await registerUser(carol);
|
||||
await joinServerFromSearch(carol.page, serverName);
|
||||
await waitForRoomReady(carol.page);
|
||||
await waitForConnectedPeerCount(alice.page, 2);
|
||||
await waitForConnectedPeerCount(carol.page, 1);
|
||||
|
||||
await expectUserRowVisible(carol.page, firstProfile.displayName);
|
||||
await expectProfileCardDetails(carol.page, firstProfile);
|
||||
});
|
||||
|
||||
await test.step('Alice changes her profile again and new chat messages use the latest display name', async () => {
|
||||
await updateProfileFromRoomSidebar(alice.page, firstProfile, secondProfile);
|
||||
|
||||
await expectUserRowVisible(alice.page, secondProfile.displayName);
|
||||
await expectUserRowVisible(bob.page, secondProfile.displayName);
|
||||
await expectUserRowVisible(carol.page, secondProfile.displayName);
|
||||
await expectProfileCardDetails(bob.page, secondProfile);
|
||||
await expectProfileCardDetails(carol.page, secondProfile);
|
||||
|
||||
const aliceMessagesPage = new ChatMessagesPage(alice.page);
|
||||
|
||||
await aliceMessagesPage.sendMessage(messageText);
|
||||
|
||||
await expectChatMessageSenderName(alice.page, messageText, secondProfile.displayName);
|
||||
await expectChatMessageSenderName(bob.page, messageText, secondProfile.displayName);
|
||||
await expectChatMessageSenderName(carol.page, messageText, secondProfile.displayName);
|
||||
});
|
||||
|
||||
await test.step('Bob, Carol, and Alice keep the latest profile after a full app restart', async () => {
|
||||
await restartPersistentClient(bob, testServer.port);
|
||||
await openRoomAfterRestart(bob, roomUrl);
|
||||
await expectUserRowVisible(bob.page, secondProfile.displayName);
|
||||
await expectProfileCardDetails(bob.page, secondProfile);
|
||||
|
||||
await restartPersistentClient(carol, testServer.port);
|
||||
await openRoomAfterRestart(carol, roomUrl);
|
||||
await expectUserRowVisible(carol.page, secondProfile.displayName);
|
||||
await expectProfileCardDetails(carol.page, secondProfile);
|
||||
|
||||
await restartPersistentClient(alice, testServer.port);
|
||||
await openRoomAfterRestart(alice, roomUrl);
|
||||
await expectUserRowVisible(alice.page, secondProfile.displayName);
|
||||
await expectProfileCardDetails(alice.page, secondProfile);
|
||||
});
|
||||
} finally {
|
||||
await Promise.all(clients.map(async (client) => {
|
||||
await closePersistentClient(client);
|
||||
await rm(client.userDataDir, { recursive: true, force: true });
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function createPersistentClient(user: TestUser, testServerPort: number): Promise<PersistentClient> {
|
||||
const userDataDir = await mkdtemp(join(tmpdir(), 'metoyou-avatar-e2e-'));
|
||||
const session = await launchPersistentSession(userDataDir, testServerPort);
|
||||
@@ -220,6 +369,8 @@ async function launchPersistentSession(
|
||||
|
||||
const page = context.pages()[0] ?? await context.newPage();
|
||||
|
||||
await installWebRTCTracking(page);
|
||||
|
||||
return { context, page };
|
||||
}
|
||||
|
||||
@@ -288,6 +439,43 @@ async function uploadAvatarFromRoomSidebar(
|
||||
await expect(applyButton).not.toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
|
||||
async function updateProfileFromRoomSidebar(
|
||||
page: Page,
|
||||
currentProfile: ProfileMetadata,
|
||||
nextProfile: ProfileMetadata
|
||||
): Promise<void> {
|
||||
const profileCard = await openProfileCardFromUserRow(page, currentProfile.displayName);
|
||||
const displayNameButton = profileCard.getByRole('button', { name: currentProfile.displayName, exact: true });
|
||||
|
||||
await expect(displayNameButton).toBeVisible({ timeout: 10_000 });
|
||||
await displayNameButton.click();
|
||||
|
||||
const displayNameInput = profileCard.locator('input[type="text"]').first();
|
||||
|
||||
await expect(displayNameInput).toBeVisible({ timeout: 10_000 });
|
||||
await displayNameInput.fill(nextProfile.displayName);
|
||||
await displayNameInput.blur();
|
||||
|
||||
await expect(profileCard.locator('input[type="text"]')).toHaveCount(0, { timeout: 10_000 });
|
||||
|
||||
const currentDescriptionText = currentProfile.description || 'Add a description';
|
||||
|
||||
await profileCard.getByText(currentDescriptionText, { exact: true }).click();
|
||||
|
||||
const descriptionInput = profileCard.locator('textarea').first();
|
||||
|
||||
await expect(descriptionInput).toBeVisible({ timeout: 10_000 });
|
||||
await descriptionInput.fill(nextProfile.description || '');
|
||||
await descriptionInput.blur();
|
||||
|
||||
await expect(profileCard.locator('textarea')).toHaveCount(0, { timeout: 10_000 });
|
||||
await expect(profileCard.getByText(nextProfile.displayName, { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
if (nextProfile.description) {
|
||||
await expect(profileCard.getByText(nextProfile.description, { exact: true })).toBeVisible({ timeout: 10_000 });
|
||||
}
|
||||
}
|
||||
|
||||
async function openRoomAfterRestart(client: PersistentClient, roomUrl: string): Promise<void> {
|
||||
await retryTransientNavigation(() => client.page.goto(roomUrl, { waitUntil: 'domcontentloaded' }));
|
||||
|
||||
@@ -332,18 +520,73 @@ async function waitForRoomReady(page: Page): Promise<void> {
|
||||
await expect(page.locator('app-rooms-side-panel').last()).toBeVisible({ timeout: 15_000 });
|
||||
}
|
||||
|
||||
async function waitForConnectedPeerCount(page: Page, count: number, timeout = 30_000): Promise<void> {
|
||||
await page.waitForFunction((expectedCount) => {
|
||||
const connections = (window as {
|
||||
__rtcConnections?: RTCPeerConnection[];
|
||||
}).__rtcConnections ?? [];
|
||||
|
||||
return connections.filter((connection) => connection.connectionState === 'connected').length >= expectedCount;
|
||||
}, count, { timeout });
|
||||
}
|
||||
|
||||
async function openProfileCardFromUserRow(page: Page, displayName: string) {
|
||||
await closeProfileCard(page);
|
||||
|
||||
const row = getUserRow(page, displayName);
|
||||
|
||||
await expect(row).toBeVisible({ timeout: 20_000 });
|
||||
await row.click();
|
||||
|
||||
const profileCard = page.locator('app-profile-card');
|
||||
|
||||
await expect(profileCard).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
return profileCard;
|
||||
}
|
||||
|
||||
async function closeProfileCard(page: Page): Promise<void> {
|
||||
const profileCard = page.locator('app-profile-card');
|
||||
|
||||
if (await profileCard.count() === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await expect(profileCard).toBeVisible({ timeout: 1_000 });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
await page.mouse.click(8, 8);
|
||||
await expect(profileCard).toHaveCount(0, { timeout: 10_000 });
|
||||
}
|
||||
|
||||
function getUserRow(page: Page, displayName: string) {
|
||||
const usersSidePanel = page.locator('app-rooms-side-panel').last();
|
||||
|
||||
return usersSidePanel.locator('[role="button"]').filter({
|
||||
has: page.getByText(displayName, { exact: true })
|
||||
}).first();
|
||||
})
|
||||
.first();
|
||||
}
|
||||
|
||||
async function expectUserRowVisible(page: Page, displayName: string): Promise<void> {
|
||||
await expect(getUserRow(page, displayName)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectProfileCardDetails(page: Page, profile: ProfileMetadata): Promise<void> {
|
||||
const profileCard = await openProfileCardFromUserRow(page, profile.displayName);
|
||||
|
||||
await expect(profileCard.getByText(profile.displayName, { exact: true })).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
if (profile.description) {
|
||||
await expect(profileCard.getByText(profile.description, { exact: true })).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
await closeProfileCard(page);
|
||||
}
|
||||
|
||||
async function expectSidebarAvatar(page: Page, displayName: string, expectedDataUrl: string): Promise<void> {
|
||||
const row = getUserRow(page, displayName);
|
||||
|
||||
@@ -400,6 +643,14 @@ async function expectChatMessageAvatar(page: Page, messageText: string, expected
|
||||
}).toBe(expectedDataUrl);
|
||||
}
|
||||
|
||||
async function expectChatMessageSenderName(page: Page, messageText: string, expectedDisplayName: string): Promise<void> {
|
||||
const messagesPage = new ChatMessagesPage(page);
|
||||
const messageItem = messagesPage.getMessageItemByText(messageText);
|
||||
|
||||
await expect(messageItem).toBeVisible({ timeout: 20_000 });
|
||||
await expect(messageItem.getByText(expectedDisplayName, { exact: true })).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function expectVoiceControlsAvatar(page: Page, expectedDataUrl: string): Promise<void> {
|
||||
const voiceControls = page.locator('app-voice-controls');
|
||||
|
||||
@@ -431,7 +682,11 @@ function buildAnimatedGifUpload(label: string): AvatarUploadPayload {
|
||||
const frame = baseGif.subarray(frameStart, baseGif.length - 1);
|
||||
const commentData = Buffer.from(label, 'ascii');
|
||||
const commentExtension = Buffer.concat([
|
||||
Buffer.from([0x21, 0xFE, commentData.length]),
|
||||
Buffer.from([
|
||||
0x21,
|
||||
0xFE,
|
||||
commentData.length
|
||||
]),
|
||||
commentData,
|
||||
Buffer.from([0x00])
|
||||
]);
|
||||
@@ -454,5 +709,6 @@ function buildAnimatedGifUpload(label: string): AvatarUploadPayload {
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36)
|
||||
.slice(2, 8)}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user