test: Add playwright main usage test
Some checks failed
Deploy Web Apps / deploy (push) Has been cancelled
Queue Release Build / prepare (push) Successful in 21s
Queue Release Build / build-linux (push) Successful in 27m44s
Queue Release Build / build-windows (push) Successful in 32m16s
Queue Release Build / finalize (push) Successful in 1m54s
Some checks failed
Deploy Web Apps / deploy (push) Has been cancelled
Queue Release Build / prepare (push) Successful in 21s
Queue Release Build / build-linux (push) Successful in 27m44s
Queue Release Build / build-windows (push) Successful in 32m16s
Queue Release Build / finalize (push) Successful in 1m54s
This commit is contained in:
295
e2e/tests/chat/chat-message-features.spec.ts
Normal file
295
e2e/tests/chat/chat-message-features.spec.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
import { test, expect, 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,
|
||||
type ChatDropFilePayload
|
||||
} from '../../pages/chat-messages.page';
|
||||
|
||||
const MOCK_EMBED_URL = 'https://example.test/mock-embed';
|
||||
const MOCK_EMBED_TITLE = 'Mock Embed Title';
|
||||
const MOCK_EMBED_DESCRIPTION = 'Mock embed description for chat E2E coverage.';
|
||||
const MOCK_GIF_IMAGE_URL = 'data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
const DELETED_MESSAGE_CONTENT = '[Message deleted]';
|
||||
|
||||
test.describe('Chat messaging features', () => {
|
||||
test.describe.configure({ timeout: 180_000 });
|
||||
|
||||
test('syncs messages in a newly created text channel', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
const channelName = uniqueName('updates');
|
||||
const aliceMessage = `Alice text channel message ${uniqueName('msg')}`;
|
||||
const bobMessage = `Bob text channel reply ${uniqueName('msg')}`;
|
||||
|
||||
await test.step('Alice creates a new text channel and both users join it', async () => {
|
||||
await scenario.aliceRoom.ensureTextChannelExists(channelName);
|
||||
await scenario.aliceRoom.joinTextChannel(channelName);
|
||||
await scenario.bobRoom.joinTextChannel(channelName);
|
||||
});
|
||||
|
||||
await test.step('Alice and Bob see synced messages in the new text channel', async () => {
|
||||
await scenario.aliceMessages.sendMessage(aliceMessage);
|
||||
await expect(scenario.bobMessages.getMessageItemByText(aliceMessage)).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
await scenario.bobMessages.sendMessage(bobMessage);
|
||||
await expect(scenario.aliceMessages.getMessageItemByText(bobMessage)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('shows typing indicators to other users', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
const draftMessage = `Typing indicator draft ${uniqueName('draft')}`;
|
||||
|
||||
await test.step('Alice starts typing in general channel', async () => {
|
||||
await scenario.aliceMessages.typeDraft(draftMessage);
|
||||
});
|
||||
|
||||
await test.step('Bob sees Alice typing', async () => {
|
||||
await expect(scenario.bob.page.getByText('Alice is typing...')).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('edits and removes messages for both users', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
const originalMessage = `Editable message ${uniqueName('edit')}`;
|
||||
const updatedMessage = `Edited message ${uniqueName('edit')}`;
|
||||
|
||||
await test.step('Alice sends a message and Bob receives it', async () => {
|
||||
await scenario.aliceMessages.sendMessage(originalMessage);
|
||||
await expect(scenario.bobMessages.getMessageItemByText(originalMessage)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
|
||||
await test.step('Alice edits the message and both users see updated content', async () => {
|
||||
await scenario.aliceMessages.editOwnMessage(originalMessage, updatedMessage);
|
||||
await expect(scenario.aliceMessages.getMessageItemByText(updatedMessage)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.alice.page.getByText('(edited)')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(scenario.bobMessages.getMessageItemByText(updatedMessage)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
|
||||
await test.step('Alice deletes the message and both users see deletion state', async () => {
|
||||
await scenario.aliceMessages.deleteOwnMessage(updatedMessage);
|
||||
await expect(scenario.aliceMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.bobMessages.getMessageItemByText(DELETED_MESSAGE_CONTENT)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('syncs image and file attachments between users', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
const imageName = `${uniqueName('diagram')}.svg`;
|
||||
const fileName = `${uniqueName('notes')}.txt`;
|
||||
const imageCaption = `Image upload ${uniqueName('caption')}`;
|
||||
const fileCaption = `File upload ${uniqueName('caption')}`;
|
||||
const imageAttachment = createTextFilePayload(imageName, 'image/svg+xml', buildMockSvgMarkup(imageName));
|
||||
const fileAttachment = createTextFilePayload(fileName, 'text/plain', `Attachment body for ${fileName}`);
|
||||
|
||||
await test.step('Alice sends image attachment and Bob receives it', async () => {
|
||||
await scenario.aliceMessages.attachFiles([imageAttachment]);
|
||||
await scenario.aliceMessages.sendMessage(imageCaption);
|
||||
|
||||
await scenario.aliceMessages.expectMessageImageLoaded(imageName);
|
||||
await expect(scenario.bobMessages.getMessageItemByText(imageCaption)).toBeVisible({ timeout: 20_000 });
|
||||
await scenario.bobMessages.expectMessageImageLoaded(imageName);
|
||||
});
|
||||
|
||||
await test.step('Alice sends generic file attachment and Bob receives it', async () => {
|
||||
await scenario.aliceMessages.attachFiles([fileAttachment]);
|
||||
await scenario.aliceMessages.sendMessage(fileCaption);
|
||||
|
||||
await expect(scenario.bobMessages.getMessageItemByText(fileCaption)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.bob.page.getByText(fileName, { exact: false })).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('renders link embeds for shared links', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
const messageText = `Useful docs ${MOCK_EMBED_URL}`;
|
||||
|
||||
await test.step('Alice shares a link in chat', async () => {
|
||||
await scenario.aliceMessages.sendMessage(messageText);
|
||||
await expect(scenario.bobMessages.getMessageItemByText(messageText)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
|
||||
await test.step('Both users see mocked link embed metadata', async () => {
|
||||
await expect(scenario.aliceMessages.getEmbedCardByTitle(MOCK_EMBED_TITLE)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.bobMessages.getEmbedCardByTitle(MOCK_EMBED_TITLE)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(scenario.bob.page.getByText(MOCK_EMBED_DESCRIPTION)).toBeVisible({ timeout: 20_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test('sends KLIPY GIF messages with mocked API responses', async ({ createClient }) => {
|
||||
const scenario = await createChatScenario(createClient);
|
||||
|
||||
await test.step('Alice opens GIF picker and sends mocked GIF', async () => {
|
||||
await scenario.aliceMessages.openGifPicker();
|
||||
await scenario.aliceMessages.selectFirstGif();
|
||||
});
|
||||
|
||||
await test.step('Bob sees GIF message sync', async () => {
|
||||
await scenario.aliceMessages.expectMessageImageLoaded('KLIPY GIF');
|
||||
await scenario.bobMessages.expectMessageImageLoaded('KLIPY GIF');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type ChatScenario = {
|
||||
alice: Client;
|
||||
bob: Client;
|
||||
aliceRoom: ChatRoomPage;
|
||||
bobRoom: ChatRoomPage;
|
||||
aliceMessages: ChatMessagesPage;
|
||||
bobMessages: ChatMessagesPage;
|
||||
};
|
||||
|
||||
async function createChatScenario(createClient: () => Promise<Client>): Promise<ChatScenario> {
|
||||
const suffix = uniqueName('chat');
|
||||
const serverName = `Chat Server ${suffix}`;
|
||||
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 installChatFeatureMocks(alice.page);
|
||||
await installChatFeatureMocks(bob.page);
|
||||
|
||||
const aliceRegisterPage = new RegisterPage(alice.page);
|
||||
const bobRegisterPage = new RegisterPage(bob.page);
|
||||
|
||||
await aliceRegisterPage.goto();
|
||||
await aliceRegisterPage.register(
|
||||
aliceCredentials.username,
|
||||
aliceCredentials.displayName,
|
||||
aliceCredentials.password
|
||||
);
|
||||
await expect(alice.page).toHaveURL(/\/search/, { timeout: 15_000 });
|
||||
|
||||
await bobRegisterPage.goto();
|
||||
await bobRegisterPage.register(
|
||||
bobCredentials.username,
|
||||
bobCredentials.displayName,
|
||||
bobCredentials.password
|
||||
);
|
||||
await expect(bob.page).toHaveURL(/\/search/, { timeout: 15_000 });
|
||||
|
||||
const aliceSearchPage = new ServerSearchPage(alice.page);
|
||||
|
||||
await aliceSearchPage.createServer(serverName, {
|
||||
description: 'E2E chat server for messaging feature coverage'
|
||||
});
|
||||
await expect(alice.page).toHaveURL(/\/room\//, { timeout: 15_000 });
|
||||
|
||||
const bobSearchPage = new ServerSearchPage(bob.page);
|
||||
const serverCard = bob.page.locator('button', { hasText: serverName }).first();
|
||||
|
||||
await bobSearchPage.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();
|
||||
|
||||
return {
|
||||
alice,
|
||||
bob,
|
||||
aliceRoom,
|
||||
bobRoom,
|
||||
aliceMessages,
|
||||
bobMessages
|
||||
};
|
||||
}
|
||||
|
||||
async function installChatFeatureMocks(page: Page): Promise<void> {
|
||||
await page.route('**/api/klipy/config', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ enabled: true })
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/klipy/gifs**', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
enabled: true,
|
||||
hasNext: false,
|
||||
results: [
|
||||
{
|
||||
id: 'mock-gif-1',
|
||||
slug: 'mock-gif-1',
|
||||
title: 'Mock Celebration GIF',
|
||||
url: MOCK_GIF_IMAGE_URL,
|
||||
previewUrl: MOCK_GIF_IMAGE_URL,
|
||||
width: 64,
|
||||
height: 64
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.route('**/api/link-metadata**', async (route) => {
|
||||
const requestUrl = new URL(route.request().url());
|
||||
const requestedTargetUrl = requestUrl.searchParams.get('url') ?? '';
|
||||
|
||||
if (requestedTargetUrl === MOCK_EMBED_URL) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
title: MOCK_EMBED_TITLE,
|
||||
description: MOCK_EMBED_DESCRIPTION,
|
||||
imageUrl: MOCK_GIF_IMAGE_URL,
|
||||
siteName: 'Mock Docs'
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ failed: true })
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTextFilePayload(name: string, mimeType: string, content: string): ChatDropFilePayload {
|
||||
return {
|
||||
name,
|
||||
mimeType,
|
||||
base64: Buffer.from(content, 'utf8').toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockSvgMarkup(label: string): string {
|
||||
return [
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="160" height="120" viewBox="0 0 160 120">',
|
||||
'<rect width="160" height="120" rx="18" fill="#0f172a" />',
|
||||
'<circle cx="38" cy="36" r="18" fill="#38bdf8" />',
|
||||
'<rect x="66" y="28" width="64" height="16" rx="8" fill="#f8fafc" />',
|
||||
'<rect x="24" y="74" width="112" height="12" rx="6" fill="#22c55e" />',
|
||||
`<text x="24" y="104" fill="#e2e8f0" font-size="12" font-family="Arial, sans-serif">${label}</text>`,
|
||||
'</svg>'
|
||||
].join('');
|
||||
}
|
||||
|
||||
function uniqueName(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
Reference in New Issue
Block a user