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
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:
264
e2e/tests/chat/notifications.spec.ts
Normal file
264
e2e/tests/chat/notifications.spec.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user