fix: multiple bug fixes
All checks were successful
Queue Release Build / prepare (push) Successful in 15s
Deploy Web Apps / deploy (push) Successful in 6m54s
Queue Release Build / build-windows (push) Successful in 16m6s
Queue Release Build / build-linux (push) Successful in 30m58s
Queue Release Build / finalize (push) Successful in 44s

isolated users, db backup, weird disconnect issues for long voice sessions,
This commit is contained in:
2026-04-24 22:19:57 +02:00
parent 44588e8789
commit 1656b8a17f
42 changed files with 1671 additions and 88 deletions

View File

@@ -0,0 +1,264 @@
import {
expect,
type Locator,
type Page
} from '@playwright/test';
import {
test,
type Client
} from '../../fixtures/multi-client';
import { RegisterPage } from '../../pages/register.page';
import { ServerSearchPage } from '../../pages/server-search.page';
import { ChatRoomPage } from '../../pages/chat-room.page';
import { ChatMessagesPage } from '../../pages/chat-messages.page';
interface DesktopNotificationRecord {
title: string;
body: string;
}
interface NotificationScenario {
alice: Client;
bob: Client;
aliceRoom: ChatRoomPage;
bobRoom: ChatRoomPage;
bobMessages: ChatMessagesPage;
serverName: string;
channelName: string;
}
test.describe('Chat notifications', () => {
test.describe.configure({ timeout: 180_000 });
test('shows desktop notifications and unread badges for inactive channels', async ({ createClient }) => {
const scenario = await createNotificationScenario(createClient);
const message = `Background notification ${uniqueName('msg')}`;
await test.step('Bob sends a message to Alice\'s inactive channel', async () => {
await clearDesktopNotifications(scenario.alice.page);
await scenario.bobRoom.joinTextChannel(scenario.channelName);
await scenario.bobMessages.sendMessage(message);
});
await test.step('Alice receives a desktop notification with the channel preview', async () => {
const notification = await waitForDesktopNotification(scenario.alice.page);
expect(notification).toEqual({
title: `${scenario.serverName} · #${scenario.channelName}`,
body: `Bob: ${message}`
});
});
await test.step('Alice sees unread badges for the room and the inactive channel', async () => {
await expect(getUnreadBadge(getSavedRoomButton(scenario.alice.page, scenario.serverName))).toHaveText('1', { timeout: 20_000 });
await expect(getUnreadBadge(getTextChannelButton(scenario.alice.page, scenario.channelName))).toHaveText('1', { timeout: 20_000 });
});
});
test('keeps unread badges visible when a muted channel suppresses desktop popups', async ({ createClient }) => {
const scenario = await createNotificationScenario(createClient);
const message = `Muted notification ${uniqueName('msg')}`;
await test.step('Alice mutes the inactive text channel', async () => {
await muteTextChannel(scenario.alice.page, scenario.channelName);
await clearDesktopNotifications(scenario.alice.page);
});
await test.step('Bob sends a message into the muted channel', async () => {
await scenario.bobRoom.joinTextChannel(scenario.channelName);
await scenario.bobMessages.sendMessage(message);
});
await test.step('Alice still sees unread badges for the room and channel', async () => {
await expect(getUnreadBadge(getSavedRoomButton(scenario.alice.page, scenario.serverName))).toHaveText('1', { timeout: 20_000 });
await expect(getUnreadBadge(getTextChannelButton(scenario.alice.page, scenario.channelName))).toHaveText('1', { timeout: 20_000 });
});
await test.step('Alice does not get a muted desktop popup', async () => {
const notificationAppeared = await waitForAnyDesktopNotification(scenario.alice.page, 1_500);
expect(notificationAppeared).toBe(false);
});
});
});
async function createNotificationScenario(createClient: () => Promise<Client>): Promise<NotificationScenario> {
const suffix = uniqueName('notify');
const serverName = `Notifications Server ${suffix}`;
const channelName = uniqueName('updates');
const aliceCredentials = {
username: `alice_${suffix}`,
displayName: 'Alice',
password: 'TestPass123!'
};
const bobCredentials = {
username: `bob_${suffix}`,
displayName: 'Bob',
password: 'TestPass123!'
};
const alice = await createClient();
const bob = await createClient();
await installDesktopNotificationSpy(alice.page);
await registerUser(alice.page, aliceCredentials.username, aliceCredentials.displayName, aliceCredentials.password);
await registerUser(bob.page, bobCredentials.username, bobCredentials.displayName, bobCredentials.password);
const aliceSearch = new ServerSearchPage(alice.page);
await aliceSearch.createServer(serverName, {
description: 'E2E notification coverage server'
});
await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 });
const bobSearch = new ServerSearchPage(bob.page);
const serverCard = bob.page.locator('button', { hasText: serverName }).first();
await bobSearch.searchInput.fill(serverName);
await expect(serverCard).toBeVisible({ timeout: 15_000 });
await serverCard.click();
await expect(bob.page).toHaveURL(/\/room\//, { timeout: 15_000 });
const aliceRoom = new ChatRoomPage(alice.page);
const bobRoom = new ChatRoomPage(bob.page);
const aliceMessages = new ChatMessagesPage(alice.page);
const bobMessages = new ChatMessagesPage(bob.page);
await aliceMessages.waitForReady();
await bobMessages.waitForReady();
await aliceRoom.ensureTextChannelExists(channelName);
await expect(getTextChannelButton(alice.page, channelName)).toBeVisible({ timeout: 20_000 });
return {
alice,
bob,
aliceRoom,
bobRoom,
bobMessages,
serverName,
channelName
};
}
async function registerUser(page: Page, username: string, displayName: string, password: string): Promise<void> {
const registerPage = new RegisterPage(page);
await registerPage.goto();
await registerPage.register(username, displayName, password);
await expect(page).toHaveURL(/\/search/, { timeout: 15_000 });
}
async function installDesktopNotificationSpy(page: Page): Promise<void> {
await page.addInitScript(() => {
const notifications: DesktopNotificationRecord[] = [];
class MockNotification {
static permission = 'granted';
static async requestPermission(): Promise<NotificationPermission> {
return 'granted';
}
onclick: (() => void) | null = null;
constructor(title: string, options?: NotificationOptions) {
notifications.push({
title,
body: options?.body ?? ''
});
}
close(): void {
return;
}
}
Object.defineProperty(window, '__desktopNotifications', {
value: notifications,
configurable: true
});
Object.defineProperty(window, 'Notification', {
value: MockNotification,
configurable: true,
writable: true
});
});
}
async function clearDesktopNotifications(page: Page): Promise<void> {
await page.evaluate(() => {
(window as WindowWithDesktopNotifications).__desktopNotifications.length = 0;
});
}
async function waitForDesktopNotification(page: Page): Promise<DesktopNotificationRecord> {
await expect.poll(
async () => (await readDesktopNotifications(page)).length,
{
timeout: 20_000,
message: 'Expected a desktop notification to be emitted'
}
).toBeGreaterThan(0);
const notifications = await readDesktopNotifications(page);
return notifications[notifications.length - 1];
}
async function waitForAnyDesktopNotification(page: Page, timeout: number): Promise<boolean> {
try {
await page.waitForFunction(
() => (window as WindowWithDesktopNotifications).__desktopNotifications.length > 0,
undefined,
{ timeout }
);
return true;
} catch (error) {
if (error instanceof Error && error.name === 'TimeoutError') {
return false;
}
throw error;
}
}
async function readDesktopNotifications(page: Page): Promise<DesktopNotificationRecord[]> {
return page.evaluate(() => {
return [...(window as WindowWithDesktopNotifications).__desktopNotifications];
});
}
async function muteTextChannel(page: Page, channelName: string): Promise<void> {
const channelButton = getTextChannelButton(page, channelName);
const contextMenu = page.locator('app-context-menu');
await expect(channelButton).toBeVisible({ timeout: 20_000 });
await channelButton.click({ button: 'right' });
await expect(contextMenu.getByRole('button', { name: 'Mute Notifications' })).toBeVisible({ timeout: 10_000 });
await contextMenu.getByRole('button', { name: 'Mute Notifications' }).click();
await expect(contextMenu).toHaveCount(0);
}
function getSavedRoomButton(page: Page, roomName: string): Locator {
return page.locator(`button[title="${roomName}"]`).first();
}
function getTextChannelButton(page: Page, channelName: string): Locator {
return page.locator('app-rooms-side-panel').first()
.locator(`button[data-channel-type="text"][data-channel-name="${channelName}"]`)
.first();
}
function getUnreadBadge(container: Locator): Locator {
return container.locator('span.rounded-full').first();
}
function uniqueName(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
interface WindowWithDesktopNotifications extends Window {
__desktopNotifications: DesktopNotificationRecord[];
}