import { expect, type Locator, type Page } from '@playwright/test'; export type ChatDropFilePayload = { name: string; mimeType: string; base64: string; }; export class ChatMessagesPage { readonly composer: Locator; readonly composerInput: Locator; readonly sendButton: Locator; readonly typingIndicator: Locator; readonly gifButton: Locator; readonly gifPicker: Locator; readonly messageItems: Locator; constructor(private page: Page) { this.composer = page.locator('app-chat-message-composer'); this.composerInput = page.getByPlaceholder('Type a message...'); this.sendButton = page.getByRole('button', { name: 'Send message' }); this.typingIndicator = page.locator('app-typing-indicator'); this.gifButton = page.getByRole('button', { name: 'Search KLIPY GIFs' }); this.gifPicker = page.getByRole('dialog', { name: 'KLIPY GIF picker' }); this.messageItems = page.locator('[data-message-id]'); } async waitForReady(): Promise { await expect(this.composerInput).toBeVisible({ timeout: 30_000 }); } async sendMessage(content: string): Promise { await this.waitForReady(); await this.composerInput.fill(content); await this.sendButton.click(); } async typeDraft(content: string): Promise { await this.waitForReady(); await this.composerInput.fill(content); } async clearDraft(): Promise { await this.waitForReady(); await this.composerInput.fill(''); } async attachFiles(files: ChatDropFilePayload[]): Promise { await this.waitForReady(); await this.composerInput.evaluate((element, payloads: ChatDropFilePayload[]) => { const dataTransfer = new DataTransfer(); for (const payload of payloads) { const binary = atob(payload.base64); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index++) { bytes[index] = binary.charCodeAt(index); } dataTransfer.items.add(new File([bytes], payload.name, { type: payload.mimeType })); } element.dispatchEvent(new DragEvent('drop', { bubbles: true, cancelable: true, dataTransfer })); }, files); } async openGifPicker(): Promise { await this.waitForReady(); await this.gifButton.click(); await expect(this.gifPicker).toBeVisible({ timeout: 10_000 }); } async selectFirstGif(): Promise { const gifCard = this.gifPicker.getByRole('button', { name: /click to select/i }).first(); await expect(gifCard).toBeVisible({ timeout: 10_000 }); await gifCard.click(); } getMessageItemByText(text: string): Locator { return this.messageItems.filter({ has: this.page.getByText(text, { exact: false }) }).last(); } getMessageImageByAlt(altText: string): Locator { return this.page.locator(`[data-message-id] img[alt="${altText}"]`).last(); } async expectMessageImageLoaded(altText: string): Promise { const image = this.getMessageImageByAlt(altText); await expect(image).toBeVisible({ timeout: 20_000 }); await expect.poll(async () => image.evaluate((element) => { const img = element as HTMLImageElement; return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0; }), { timeout: 20_000, message: `Image ${altText} should fully load in chat` }).toBe(true); } getEmbedCardByTitle(title: string): Locator { return this.page.locator('app-chat-link-embed').filter({ has: this.page.getByText(title, { exact: true }) }).last(); } async editOwnMessage(originalText: string, updatedText: string): Promise { const messageItem = this.getMessageItemByText(originalText); const editButton = messageItem.locator('button:has(ng-icon[name="lucideEdit"])').first(); const editTextarea = this.page.locator('textarea.edit-textarea').first(); const saveButton = this.page.locator('button:has(ng-icon[name="lucideCheck"])').first(); await expect(messageItem).toBeVisible({ timeout: 15_000 }); await messageItem.hover(); await editButton.click(); await expect(editTextarea).toBeVisible({ timeout: 10_000 }); await editTextarea.fill(updatedText); await saveButton.click(); } async deleteOwnMessage(text: string): Promise { const messageItem = this.getMessageItemByText(text); const deleteButton = messageItem.locator('button:has(ng-icon[name="lucideTrash2"])').first(); await expect(messageItem).toBeVisible({ timeout: 15_000 }); await messageItem.hover(); await deleteButton.click(); } }