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,
265 lines
8.8 KiB
TypeScript
265 lines
8.8 KiB
TypeScript
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[];
|
|
}
|